diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 90c8a18..b261ec8 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -74,6 +74,18 @@ jobs:
{ printf -- "$front"; cat _app/CHANGELOG.md; } > source/_reference/changelog.md
{ printf -- "$front6"; cat _app6/CHANGELOG.md; } > source/v6/_reference/changelog.md
+ # The published security advisories of the app, for the Security advisories page
+ # (source/_security_assessment/security-assessment/advisories.md). Fetched on every build, so a
+ # new advisory reaches the site with the next build; a failed fetch fails the build rather than
+ # publishing a list that silently lost its entries.
+ - name: Bring in the security advisories
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ gh api 'repos/ansibleforms/ansibleforms/security-advisories?state=published&per_page=100' > source/_data/advisories.json
+ echo "$(jq length source/_data/advisories.json) published advisories"
+
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
diff --git a/.gitignore b/.gitignore
index 058b7dc..cf6ce30 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,6 +12,7 @@ vendor/
_app/
_app6/
source/_data/help.yaml
+source/_data/advisories.json
# A language other than English is assembled here before it is built (scripts/build-language.sh)
_build/
diff --git a/i18n/es.yml b/i18n/es.yml
index ffe9343..1a72e08 100644
--- a/i18n/es.yml
+++ b/i18n/es.yml
@@ -27,6 +27,8 @@ just_the_docs:
name: Guía de usuario
integrate:
name: Integrar
+ security_assessment:
+ name: Seguridad
reference:
name: Referencia
diff --git a/source/_config.yml b/source/_config.yml
index dcfe759..9c2ee20 100644
--- a/source/_config.yml
+++ b/source/_config.yml
@@ -126,6 +126,9 @@ collections:
integrate:
permalink: "/:path:output_ext"
output: true
+ security_assessment:
+ permalink: "/:path:output_ext"
+ output: true
reference:
permalink: "/:path:output_ext"
output: true
@@ -156,6 +159,10 @@ just_the_docs:
name: Integrate
icon: plug
nav_fold: true
+ security_assessment:
+ name: Security
+ icon: shield-halved
+ nav_fold: true
reference:
name: Reference
icon: book
diff --git a/source/_security_assessment/security-assessment/advisories.md b/source/_security_assessment/security-assessment/advisories.md
new file mode 100644
index 0000000..6c1cec5
--- /dev/null
+++ b/source/_security_assessment/security-assessment/advisories.md
@@ -0,0 +1,79 @@
+---
+layout: default
+title: Security advisories
+nav_order: 5
+---
+
+# Security advisories
+{: .no_toc }
+
+Every published vulnerability, its severity and its fix
+{: .fs-6 .fw-300 }
+
+1. TOC
+{:toc}
+
+---
+
+## Published advisories
+
+Read from the [Security tab of the repository](https://github.com/ansibleforms/ansibleforms/security/advisories) at every build of this site:
+
+{% assign af_advisories = site.data.advisories | sort: "published_at" | reverse %}
+{% if af_advisories.size > 0 %}
+
+
+
+ | Advisory |
+ Severity |
+ Weakness |
+ Affected |
+ Fixed in |
+ Published |
+
+
+
+ {% for a in af_advisories %}
+
+
+ {{ a.summary | escape }}
+ {{ a.ghsa_id }}{% if a.cve_id %} {{ a.cve_id }}{% endif %}
+ |
+
+ {{ a.severity | capitalize }}
+ {%- assign af_cvss = a.cvss_severities.cvss_v3 | default: a.cvss -%}
+ {%- if af_cvss.score %} CVSS {{ af_cvss.score }}{% endif %}
+ |
+
+ {%- for w in a.cwes %}{{ w.cwe_id }}{% unless forloop.last %} {% endunless %}{% endfor -%}
+ |
+ {% for v in a.vulnerabilities %}{{ v.vulnerable_version_range }}{% unless forloop.last %} {% endunless %}{% endfor %} |
+ {% for v in a.vulnerabilities %}{{ v.patched_versions | default: "-" }}{% unless forloop.last %} {% endunless %}{% endfor %} |
+ {{ a.published_at | date: "%Y-%m-%d" }} |
+
+ {% endfor %}
+
+
+{% else %}
+No advisory has been published.
+{% endif %}
+
+Each advisory on GitHub gives the details: the impact, the conditions, the CVSS vector and the fix.
+
+---
+
+## What this means for your version
+
+An advisory names the versions it affects and the first version that is fixed. The fix ships in the supported releases,
+[7.x and 6.5.x](development.html#supported-versions), and older releases are not patched: upgrading is the fix.
+
+Compare the version of an instance (**About** in the help menu, or `GET /api/v2/version`) with **Affected**.
+
+---
+
+## Being told of new advisories
+
+GitHub notifies the watchers of the repository when an advisory is published:
+
+* On [github.com/ansibleforms/ansibleforms](https://github.com/ansibleforms/ansibleforms), choose **Watch > Custom > Security alerts**.
+* The [changelog](../changelog.html) lists every security fix under the release that ships it.
diff --git a/source/_security_assessment/security-assessment/development.md b/source/_security_assessment/security-assessment/development.md
new file mode 100644
index 0000000..b5b66a3
--- /dev/null
+++ b/source/_security_assessment/security-assessment/development.md
@@ -0,0 +1,91 @@
+---
+layout: default
+title: Secure development
+nav_order: 3
+---
+
+# Secure development
+{: .no_toc }
+
+How changes are checked and released, and what is supported
+{: .fs-6 .fw-300 }
+
+1. TOC
+{:toc}
+
+---
+
+## The source code
+
+AnsibleForms is open source, so everything on this page can be checked in the repository itself:
+
+* The code, its history and its build pipelines are public on [github.com/ansibleforms/ansibleforms](https://github.com/ansibleforms/ansibleforms).
+* The [license](https://github.com/ansibleforms/ansibleforms/blob/main/LICENSE) is GPL-3.0.
+* The [changelog](../changelog.html) lists every change of every release, security fixes included.
+
+---
+
+## How a change is merged
+
+Every change reaches `main` through a pull request, under a ruleset that nobody can bypass:
+
+| Rule | What it means |
+|---|---|
+| Pull request required | Nothing is pushed to `main` directly |
+| Required checks | The pull request merges only when every check below passes |
+| No force push, no deletion | The history of `main` cannot be rewritten or removed |
+| Linear history | Every pull request becomes one commit, so each change can be traced and reverted on its own |
+
+The checks a pull request must pass:
+
+| Check | What it does |
+|---|---|
+| Client, Server | The unit tests of the web interface and of the server |
+| CodeQL | GitHub's static analysis of the JavaScript and TypeScript code, for injection, cross-site scripting and other weaknesses |
+| Public schema is up to date | The database schema the code expects matches the one it ships |
+| Title, Branch | The pull request follows the naming rules, so the changelog stays accurate |
+
+Two more run on every pull request and report on it:
+
+* **Database** tests the server on MySQL and MariaDB, and **Browser** drives the interface in a browser.
+* **Dependency review** blocks a new dependency with a known high or critical vulnerability.
+
+---
+
+## Code analysis and dependencies
+
+The code and what it depends on are checked again after they merge:
+
+* **CodeQL** also analyses every push to `main` and to the release branches, and the whole code base every week:
+ a new CodeQL rule then also checks the code merged before it.
+* **Dependabot** opens a pull request for each new version of a dependency: the npm packages of the server and the
+ interface, the Python packages of the runtime environment, and the GitHub Actions of the pipelines every week, and
+ the base images of the containers every day.
+* The base images are pinned by digest (`node:26-trixie-slim@sha256:...`), so a build uses exactly the reviewed image.
+ Dependabot moves the digest when the image is rebuilt upstream.
+
+---
+
+## Releases and images
+
+A release is built and published by GitHub Actions only, never from a developer's machine:
+
+* A release is cut from `main` (or `release/6.x`), with a changelog built from its pull requests.
+* The images are built by the [Publish workflow](https://github.com/ansibleforms/ansibleforms/blob/main/.github/workflows/publish.yml)
+ and pushed to the GitHub Container Registry, `ghcr.io/ansibleforms/ansibleforms`, with the runtime environment images
+ next to it. The workflow and its logs are public, so anyone can see what went into an image.
+* The application and runtime environment images run as the unprivileged `node` user (uid 1000), never as root.
+
+---
+
+## Supported versions
+
+Security fixes go into the newest release of each supported line:
+
+| Version | Receives security fixes |
+|---|---|
+| 7.x | Yes |
+| 6.5.x | Yes |
+| Older than 6.5 | No: upgrading is the fix |
+
+The [upgrade guide](../upgrading/) covers moving to a newer release, and [Upgrading to v7](../upgrade-7.html) the move from 6.5.
diff --git a/source/_security_assessment/security-assessment/faq.md b/source/_security_assessment/security-assessment/faq.md
new file mode 100644
index 0000000..731a656
--- /dev/null
+++ b/source/_security_assessment/security-assessment/faq.md
@@ -0,0 +1,85 @@
+---
+layout: default
+title: Assessment FAQ
+nav_order: 6
+---
+
+# Assessment FAQ
+{: .no_toc }
+
+Short answers to the questions security questionnaires ask most
+{: .fs-6 .fw-300 }
+
+1. TOC
+{:toc}
+
+---
+
+## Data and hosting
+
+Where the data lives, and who can reach it:
+
+| Question | Answer |
+|---|---|
+| Where is our data stored? | In the MySQL or MariaDB database you run, and in the persistent folder of your instance. AnsibleForms is self-hosted, with no cloud service behind it. |
+| Does it send data to its makers? | No. There is no telemetry, no usage reporting and no update check. |
+| What does it connect to? | Only the systems an administrator configures: see [Outbound connections](measures.html#outbound-connections). |
+| Does it use AI or send data to a model provider? | Only with the [chat assistant](../chat/) on, which is off by default. It then sends form definitions and the values being filled in to the provider you configure. |
+| Can it run without internet access? | Yes. The application needs no internet access; the playbooks need whatever their tasks reach. |
+
+---
+
+## Encryption
+
+What is encrypted, and with which keys:
+
+| Question | Answer |
+|---|---|
+| Is data encrypted in transit? | With [HTTPS](../security/https.html), served by the application or by your reverse proxy, and `wss://` between the application and its runtime environments. |
+| Is data encrypted at rest? | Every stored secret is encrypted by the application with AES-256. Jobs and their output are stored as they are: use the database's own encryption at rest. See [Stored secrets](measures.html#stored-secrets). |
+| Who holds the keys? | You do: the [`ENCRYPTION_SECRET`](../security/secrets.html) is set by you, kept out of backups, and can be rotated. |
+| Can secrets stay in our vault? | Yes: [HashiCorp Vault and CyberArk](../secret-stores/) are read when a job needs them. |
+
+---
+
+## Access and accountability
+
+Who can sign in, what they can do, and what is recorded:
+
+| Question | Answer |
+|---|---|
+| Does it support single sign-on? | Yes: LDAP / Active Directory, OpenID Connect and Entra ID. See [Authentication](../authentication/). |
+| Does it support multi-factor authentication? | Through single sign-on: your identity provider's MFA applies. Local accounts have none of their own. |
+| How are permissions managed? | With roles mapped to groups, checked by the server on every request. See [Permissions](measures.html#permissions). |
+| Is there an audit log? | Yes: every change and every refused attempt, kept 365 days by default and forwardable to syslog. See [Audit trail](measures.html#audit-trail). |
+| Is there a default password? | The first admin account has a public default password, which must be changed at the first sign-in. See [The admin account](../security/hardening.html#the-admin-account). |
+
+---
+
+## Vulnerabilities and updates
+
+How problems are found, fixed and announced:
+
+| Question | Answer |
+|---|---|
+| Have vulnerabilities been found? | Yes. Every published one is listed in [Security advisories](advisories.html), with the version that fixed it. |
+| How do we report one? | Privately, through GitHub: see [Reporting a vulnerability](reporting.html). |
+| How is the code checked? | CodeQL static analysis and dependency review on every pull request, automated tests, and Dependabot updates. See [Secure development](development.html). |
+| Which versions get security fixes? | 7.x and 6.5.x. See [Supported versions](development.html#supported-versions). |
+| How do we hear of a new advisory? | Watch the repository's security alerts on GitHub. See [Being told of new advisories](advisories.html#being-told-of-new-advisories). |
+| Has it been penetration tested? | No third-party penetration test report is published. The code is public, so your own team or tester can assess it, and findings are reported as above. |
+
+---
+
+## Images and supply chain
+
+Where the images come from, and how to check them:
+
+| Question | Answer |
+|---|---|
+| Where are the images published? | On the GitHub Container Registry, `ghcr.io/ansibleforms/ansibleforms`, built by public GitHub Actions workflows. See [Releases and images](development.html#releases-and-images). |
+| Do the containers run as root? | No: as the unprivileged `node` user. |
+| Are the images signed? | Not yet. Pull them by digest (`@sha256:...`) to be sure you run the image you reviewed. |
+| Is there an SBOM? | None is published with the images yet. A scanner such as Trivy or Syft builds one from the image. |
+| Are the images scanned for vulnerabilities? | Not in the release pipeline. Their base images are pinned by digest and kept current by Dependabot; scan the images in your own registry to apply your own policy. |
+| What license is it under? | GPL-3.0, open source, without warranty. |
diff --git a/source/_security_assessment/security-assessment/index.md b/source/_security_assessment/security-assessment/index.md
new file mode 100644
index 0000000..4357493
--- /dev/null
+++ b/source/_security_assessment/security-assessment/index.md
@@ -0,0 +1,39 @@
+---
+layout: default
+title: Overview
+nav_order: 1
+permalink: /security-assessment/
+---
+
+# Security assessment
+{: .no_toc }
+
+For security reviews: measures, development and advisories
+{: .fs-6 .fw-300 }
+
+---
+
+## In short
+
+AnsibleForms runs playbooks and holds their credentials, so it is part of your trust chain:
+
+* It is **self-hosted**: it runs on your infrastructure, keeps its data in your database, and sends nothing to its makers.
+* It is **open source** (GPL-3.0): the code, its pipelines and every advisory are public.
+* Every stored secret is **encrypted**, every change **audited**, and **roles** checked on every request.
+* Vulnerabilities are **reported privately**, fixed in every supported release, and **published**.
+
+---
+
+## In this section
+
+The pages answer the questions of a security review, from the design to the CVE list:
+
+| Page | What it covers |
+|---|---|
+| [Security measures](measures.html) | Sign-in, permissions, stored secrets, the audit trail, the browser, the server, connections and containers |
+| [Secure development](development.html) | How a change is reviewed and tested, code and dependency analysis, how images are built, supported versions |
+| [Reporting a vulnerability](reporting.html) | How to report a problem privately, and what happens next |
+| [Security advisories](advisories.html) | Every published vulnerability, with its CVE, severity and fixed version |
+| [Assessment FAQ](faq.html) | Short answers to the questions security questionnaires ask most |
+
+To secure an instance rather than assess it, see [Security](../security/) in the administrator's guide.
diff --git a/source/_security_assessment/security-assessment/measures.md b/source/_security_assessment/security-assessment/measures.md
new file mode 100644
index 0000000..1ceeb1a
--- /dev/null
+++ b/source/_security_assessment/security-assessment/measures.md
@@ -0,0 +1,156 @@
+---
+layout: default
+title: Security measures
+nav_order: 2
+---
+
+# Security measures
+{: .no_toc }
+
+How sign-in, permissions, secrets and connections are protected
+{: .fs-6 .fw-300 }
+
+1. TOC
+{:toc}
+
+---
+
+## Where it runs
+
+AnsibleForms is self-hosted, on your infrastructure, with its data in a database you run.
+
+* There is no AnsibleForms cloud service or licence server, and no telemetry or update check.
+* The interface is served by the application itself and loads nothing from another site.
+* The application connects only to the systems an administrator configures, listed in [Outbound connections](#outbound-connections).
+
+The parts and what each one holds:
+
+| Part | What it does | What it holds |
+|---|---|---|
+| Application | The web interface, the REST API, the scheduler | No data of its own: everything is in the database |
+| MySQL or MariaDB | The application's database | Users, groups, credentials (encrypted), jobs with their extravars and output, the audit log |
+| Runtime environment (RTE) | Runs the playbooks, in its own container | The files of the jobs it runs, and a job's secrets only while that job runs |
+| Persistent folder | The configuration, forms, playbooks and uploads | The files the forms and playbooks use |
+
+See [How it works](../how-it-works.html) for the full picture, and [RTE](../rte/) for the runtime environment.
+
+---
+
+## Signing in
+
+Every page and API call that reads or changes data needs a signed-in user. The few endpoints open without one serve the
+sign-in itself, the version, the name and logo of the instance, the API documentation, and the database setup of a first start,
+which [`ALLOW_SCHEMA_CREATION`](../security/hardening.html#the-database)`=0` closes:
+
+| Method | How it works |
+|---|---|
+| Local accounts | Passwords hashed with bcrypt, a [minimum length](../security/hardening.html#sign-in-limits-and-passwords), and the public default admin password must be changed at the first sign-in |
+| LDAP / Active Directory | The directory checks the password, and its groups map to roles. See [LDAP](../authentication/ldap.html) |
+| OpenID Connect, Entra ID | Single sign-on through your identity provider, which also brings its own multi-factor authentication. See [Authentication](../authentication/) |
+
+* Failed sign-ins [lock the account](../security/hardening.html#sign-in-limits-and-passwords), and the sign-in is rate limited.
+* A sign-in returns a signed access token valid 30 minutes and a refresh token valid 24 hours.
+* Tokens can be ended before they expire: signing out, a password change, a blocked user and a revoked API token end them on
+ every node at once, see [Tokens](../security/secrets.html#tokens).
+* AnsibleForms has no multi-factor authentication of its own: use single sign-on where it is required.
+
+---
+
+## Permissions
+
+What a user may do comes from the roles of their groups, checked by the server on every request:
+
+* Roles are defined in [config.yaml](../config/roles.html): which forms a user sees, and which options they have,
+ such as the designer, the settings pages or the jobs of other users.
+* The server checks every launch against the form's rules (its fields, their values and the user's access), including
+ launches through the REST API, see [Launch validation](../launch-validation/).
+* The admin role runs any playbook by design. Other roles get only what is granted: a user with several roles gets an
+ option only when none of those roles refuses it, so one restrictive role is enough to withhold it.
+
+---
+
+## Stored secrets
+
+Passwords, tokens and keys are encrypted before they are written to the database:
+
+* Every stored secret, credentials, the LDAP bind password, runner tokens, single sign-on client secrets, webhook
+ addresses and API keys, is encrypted with AES-256 and a random initialisation vector per value, under the
+ [`ENCRYPTION_SECRET`](../security/secrets.html#the-two-master-secrets) you set.
+* No API call or settings page shows a stored secret, and backups omit the master secrets.
+* Secrets can stay in your vault: [HashiCorp Vault and CyberArk](../secret-stores/) are read when a job needs them.
+* A job's secrets reach the runtime environment sealed with AES-256-GCM, under a key derived from its token.
+* Extravars whose name looks like a secret are masked in the job view, and password fields always are, see [Job data](../security/hardening.html#job-data).
+
+The rest of the database, jobs, their extravars and their output, is stored as it is: protect it with the database's own
+encryption at rest and access control, see [The database](../security/hardening.html#the-database).
+
+---
+
+## Audit trail
+
+Every change, and every attempt that is refused, is recorded:
+
+* Each create, change and delete, through the interface or the API, is written to the audit log with the user, their
+ address, the action, the object and the outcome. Refusals and sign-ins are recorded too, failed ones included.
+* Values that look like secrets are redacted before they are written.
+* The log is kept 365 days by default, and can be copied to syslog over UDP, TCP or TLS.
+
+---
+
+## In the browser
+
+The interface is protected against code injected into a page:
+
+* A [Content-Security-Policy](../security/hardening.html#the-content-security-policy) allows only the application's own scripts and frames.
+* Every answer carries protective headers: nosniff, frame protection, and HSTS over HTTPS.
+* The HTML of forms, help texts and chat answers is sanitized, and job output is escaped.
+
+---
+
+## On the server
+
+The code that handles user input avoids the classic injections:
+
+* Database queries pass their values as parameters, never as text glued into the query.
+* Commands, git included, run from an argument list, never a shell, so shell characters do nothing.
+* Server expressions pass an [expression sanitizer](../security/hardening.html#expressions-and-outbound-calls), and their REST calls a host filter.
+* The [MCP server](../mcp/), the [chat assistant](../chat/) and the metrics endpoint are off until you turn them on.
+
+---
+
+## Connections
+
+Traffic to and from AnsibleForms can be encrypted end to end:
+
+* The application serves [HTTPS](../security/https.html) itself, or sits behind your reverse proxy.
+* A runtime environment talks to the application over a WebSocket, `wss://` with an HTTPS address, and the application
+ pins the certificate of an inbound one, see [RTE modes](../rte/modes.html).
+* LDAP, single sign-on, the runners, git, the secret stores and the REST calls of expressions check the certificate of the
+ server they reach, unless an administrator turns the check off for a server with a self-signed certificate.
+
+### Outbound connections
+
+AnsibleForms connects only to what an administrator configures:
+
+| Connection | When |
+|---|---|
+| LDAP, OpenID Connect, Entra ID (Microsoft login and Graph) | When that sign-in method is set up |
+| AWX / AAP / Ascender, Semaphore, Rundeck, Jenkins | When a form runs on that runner |
+| Git servers | When a repository is set up |
+| Mail server, Slack, Teams, Telegram, webhooks | When a notification channel is set up |
+| HashiCorp Vault, CyberArk | When a secret store is set up |
+| A model provider (Anthropic, OpenAI and others) | Only with the [chat assistant](../chat/) on, which is off by default |
+| Hosts and APIs named in forms | When a form's expressions or a playbook call them |
+| Syslog servers | When audit or log forwarding is set up |
+
+---
+
+## The containers
+
+The images are built to run with as little privilege as possible:
+
+* The application and the runtime environment run as the unprivileged `node` user, not as root.
+* They are built on slim Debian images pinned by digest, see [Secure development](development.html#code-analysis-and-dependencies).
+* The application listens on one port, 8000.
+
+For the steps that secure a new instance, follow [Hardening](../security/hardening.html).
diff --git a/source/_security_assessment/security-assessment/reporting.md b/source/_security_assessment/security-assessment/reporting.md
new file mode 100644
index 0000000..382775f
--- /dev/null
+++ b/source/_security_assessment/security-assessment/reporting.md
@@ -0,0 +1,65 @@
+---
+layout: default
+title: Reporting a vulnerability
+nav_order: 4
+---
+
+# Reporting a vulnerability
+{: .no_toc }
+
+How to report a security problem privately, and what happens next
+{: .fs-6 .fw-300 }
+
+1. TOC
+{:toc}
+
+---
+
+## Report it privately
+
+AnsibleForms holds credentials and vault tokens, so never report a security problem in a public issue:
+
+1. Open **[Report a vulnerability](https://github.com/ansibleforms/ansibleforms/security/advisories/new)**: a private advisory only the maintainers see.
+2. If that page is not available to you, open a normal issue that says only that you have a security report and asks
+ for a private channel: no details, no reproducer, no version numbers. A maintainer opens a private advisory and
+ invites you to it.
+
+The full policy is the repository's [SECURITY.md](https://github.com/ansibleforms/ansibleforms/blob/main/SECURITY.md).
+
+---
+
+## What to include
+
+The more of this a report gives, the faster it is fixed:
+
+* What an attacker gains: another user's data, a playbook they may not run, the host, or admin rights.
+* The version, and how it is deployed: Docker, Kubernetes, or installed natively.
+* A reproducer, or the request that triggers it.
+* Whether it needs a signed-in user, and with which [role options](../config/roles.html).
+
+---
+
+## What happens next
+
+A report goes through the same private advisory from the first message to the publication:
+
+| Step | What you get |
+|---|---|
+| Acknowledgement | Confirmation that the report arrived, and whether it reproduces |
+| Accepted | A fix in the next release of every [supported version](development.html#supported-versions), and a published advisory that credits you, unless you would rather not be named |
+| Declined | The reasoning, usually that the behaviour needs a permission the reporter already had |
+
+Once the fix is released, the advisory is published and appears in [Security advisories](advisories.html).
+
+---
+
+## Scope
+
+The policy draws the line where the role model draws it:
+
+| In scope | Out of scope |
+|---|---|
+| Anything a signed-in user can reach beyond what their role grants | Findings that need the admin account to be compromised already: an admin running any playbook is the product |
+| Anything reachable without signing in | Scanner reports with no demonstrated impact |
+| Stored credentials becoming readable, or written to a log | |
+| Injection into the shell, the database or the browser | |
diff --git a/source/assets/css/custom.css b/source/assets/css/custom.css
index d1802f3..29ee142 100644
--- a/source/assets/css/custom.css
+++ b/source/assets/css/custom.css
@@ -256,3 +256,13 @@ pre.highlight .language-treeview {
margin-right: 0;
}
}
+
+/* Paragraphs, list items and table cells never end on a word or two alone on their last line, at any
+ width : the browser evens out the lines of a short one (balance, up to six lines) and
+ rebalances the last lines of a longer one (pretty). Browsers without them wrap as before */
+.main-content p,
+.main-content li,
+.main-content td {
+ text-wrap: pretty;
+ text-wrap: balance;
+}