Skip to content

Commit

Permalink
Merge branch 'main' into typing
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-shaw committed Oct 15, 2024
2 parents d96fcf2 + 73a69b3 commit a563ee4
Show file tree
Hide file tree
Showing 31 changed files with 375 additions and 463 deletions.
63 changes: 31 additions & 32 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ name: "CodeQL"

on:
push:
branches: [ "main", "develop" ]
branches: ["main", "develop"]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
branches: ["main"]
schedule:
- cron: '40 16 * * 6'
- cron: "40 16 * * 6"

jobs:
analyze:
Expand All @@ -38,45 +38,44 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'javascript-typescript', 'python' ]
language: ["javascript-typescript", "python"]
# CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
# Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
# Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.

# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality

# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2

# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.

# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh

# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"
8 changes: 4 additions & 4 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
#
# Source repository: https://github.com/actions/dependency-review-action
# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement
name: 'Dependency review'
name: "Dependency review"
on:
pull_request:
branches: [ "main" ]
branches: ["main"]

# If using a dependency submission action in this workflow this permission will need to be set to:
#
Expand All @@ -27,9 +27,9 @@ jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
- name: "Checkout repository"
uses: actions/checkout@v4
- name: 'Dependency Review'
- name: "Dependency Review"
uses: actions/dependency-review-action@v4
# Commonly enabled options, see https://github.com/actions/dependency-review-action#configuration-options for all available options.
with:
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ name: Python application

on:
push:
branches: [ "main" ]
branches: ["main"]
pull_request:
branches: [ "main" ]
branches: ["main"]

permissions:
contents: read
Expand All @@ -27,7 +27,7 @@ jobs:
pip install -r requirements_dev.txt
pip install -r requirements.txt
- name: Check dependencies for known security vulnerabilities
run: safety check -r requirements.txt
run: pip-audit -r requirements.txt
- name: Check code for potential security vulnerabilities
run: bandit -r . -x /tests
- name: Check code formatting
Expand Down
18 changes: 7 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
FROM python:3.12-slim

RUN useradd containeruser
RUN useradd appuser

WORKDIR /home/containeruser

COPY app app
COPY govuk-frontend-flask.py config.py docker-entrypoint.sh requirements.txt ./
RUN pip install -r requirements.txt \
&& chmod +x docker-entrypoint.sh \
&& chown -R containeruser:containeruser ./
WORKDIR /home/appuser

# Set environment variables
ENV FLASK_APP=govuk-frontend-flask.py \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1

USER containeruser
COPY app app
COPY govuk-frontend-flask.py config.py requirements.txt ./
RUN pip install -r requirements.txt \
&& chown -R appuser:appuser ./

EXPOSE 9876
ENTRYPOINT ["./docker-entrypoint.sh"]
USER appuser
56 changes: 45 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# GOV.UK Frontend Flask

![govuk-frontend 5.4.0](https://img.shields.io/badge/govuk--frontend%20version-5.4.0-005EA5?logo=gov.uk&style=flat)
![govuk-frontend 5.7.1](https://img.shields.io/badge/govuk--frontend%20version-5.7.1-005EA5?logo=gov.uk&style=flat)

**GOV.UK Frontend Flask is a [community tool](https://design-system.service.gov.uk/community/resources-and-tools/) of the [GOV.UK Design System](https://design-system.service.gov.uk/). The Design System team is not responsible for it and cannot support you with using it. Contact the [maintainers](#contributors) directly if you need [help](#support) or you want to request a feature.**

Expand Down Expand Up @@ -35,7 +35,7 @@ In the `compose.yml` file you will find a number of environment variables. These
- SERVICE_PHASE
- SERVICE_URL

You must also set a new unique `SECRET_KEY`, which is used to securely sign the session cookie and CSRF tokens. It should be a long random `bytes` or `str`. You can use the output of this Python comand to generate a new key:
You must also set a new unique `SECRET_KEY`, which is used to securely sign the session cookie and CSRF tokens. It should be a long random `bytes` or `str`. You can use the output of this Python command to generate a new key:

```shell
python -c 'import secrets; print(secrets.token_hex())'
Expand All @@ -53,7 +53,7 @@ python -c 'import secrets; print(secrets.token_hex())'
docker compose up --build
```

You should now have the app running on <https://localhost:9876/>. Accept the browsers security warning due to the self-signed HTTPS certificate to continue.
You should now have the app running on <https://localhost/>. Accept the browsers security warning due to the self-signed HTTPS certificate to continue.

## Demos

Expand All @@ -67,19 +67,52 @@ To run the tests:
python -m pytest --cov=app --cov-report=term-missing --cov-branch
```

## Environment

```mermaid
flowchart TB
cache1(Redis):::CACHE
Client
prox1(NGINX):::PROXY
web1(Flask app):::WEB
web2[/Static files/]:::WEB
Client <-- https:443 --> prox1 <-- http:5000 --> web1
prox1 -- Read only --> web2
web1 -- Write --> web2
web1 <-- redis:6379 --> cache1
subgraph Proxy container
prox1
end
subgraph Web container
web1
web2
end
subgraph Cache container
cache1
end
classDef CACHE fill:#F8CECC,stroke:#B85450,stroke-width:2px
classDef PROXY fill:#D5E8D4,stroke:#82B366,stroke-width:2px
classDef WEB fill:#FFF2CC,stroke:#D6B656,stroke-width:2px
```

## Features

Please refer to the specific packages documentation for more details.

### Asset management

Custom CSS and JavaScript files are merged and compressed using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). This takes all `*.css` files in `app/static/src/css` and all `*.js` files in `app/static/src/js` and outputs a single compressed file to both `app/static/dist/css` and `app/static/dist/js` respectively.
Custom CSS and JavaScript files are merged and minified using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). This takes all `*.css` files in `app/static/src/css` and all `*.js` files in `app/static/src/js` and outputs a single minified file to both `app/static/dist/css` and `app/static/dist/js` respectively.

CSS is [minified](https://en.wikipedia.org/wiki/Minification_(programming)) using [CSSMin](https://github.com/zacharyvoase/cssmin) and JavaScript is minified using [JSMin](https://github.com/tikitu/jsmin/). This removes all whitespace characters, comments and line breaks to reduce the size of the source code, making its transmission over a network more efficient.
CSS is [minified](<https://en.wikipedia.org/wiki/Minification_(programming)>) using [CSSMin](https://github.com/zacharyvoase/cssmin) and JavaScript is minified using [JSMin](https://github.com/tikitu/jsmin/). This removes all whitespace characters, comments and line breaks to reduce the size of the source code, making its transmission over a network more efficient.

### Cache busting

Merged and compressed assets are browser cache busted on update by modifying their URL with their MD5 hash using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). The MD5 hash is appended to the file name, for example `custom-d41d8cd9.css` instead of a query string, to support certain older browsers and proxies that ignore the querystring in their caching behaviour.
Merged and minified assets are browser cache busted on update by modifying the filename with their MD5 hash using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). The MD5 hash is appended to the file name, for example `custom-d41d8cd9.css` instead of a query string, to support certain older browsers and proxies that ignore the querystring in their caching behaviour.

### Forms generation and validation

Expand All @@ -101,20 +134,21 @@ CSRF errors are handled by creating a [flash message](#flash-messages) notificat

### HTTP security headers

Uses [Flask Talisman](https://github.com/GoogleCloudPlatform/flask-talisman) to set HTTP headers that can help protect against a few common web application security issues.

- Forces all connections to `https`, unless running with debug enabled.
- Forces all connections to `https`.
- Enables [HTTP Strict Transport Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security).
- Sets Flask's session cookie to `secure`, so it will never be set if your application is somehow accessed via a non-secure connection.
- Sets Flask's session cookie to `httponly`, preventing JavaScript from being able to access its content.
- Sets [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) to `SAMEORIGIN` to avoid [clickjacking](https://en.wikipedia.org/wiki/Clickjacking).
- Sets [X-XSS-Protection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection) to enable a cross site scripting filter for IE and Safari (note Chrome has removed this and Firefox never supported it).
- Sets [X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) to prevent content type sniffing.
- Sets a strict [Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) of `strict-origin-when-cross-origin` that governs which referrer information should be included with requests made.

### Content Security Policy

A strict default [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) is set using [Flask Talisman](https://github.com/GoogleCloudPlatform/flask-talisman) to mitigate [Cross Site Scripting](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) (XSS) and packet sniffing attacks. This prevents loading any resources that are not in the same domain as the application.
A strict [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) is set to mitigate [Cross Site Scripting](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) (XSS) and packet sniffing attacks. This prevents loading any resources that are not in the same domain as the application by default.

### Permissions Policy

A strict [Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy) is set to deny the use of browser features by default.

### Response compression

Expand Down
81 changes: 16 additions & 65 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@

from flask import Flask
from flask_assets import Bundle, Environment
from flask_compress import Compress
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from flask_talisman import Talisman
from flask_wtf.csrf import CSRFProtect
from govuk_frontend_wtf.main import WTFormsHelpers
from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader
from werkzeug.middleware.proxy_fix import ProxyFix

from config import Config

assets = Environment()
compress = Compress()
csrf = CSRFProtect()
limiter = Limiter(get_remote_address, default_limits=["2 per second", "60 per minute"])
talisman = Talisman()
limiter = Limiter(
get_remote_address,
default_limits=["2 per second", "60 per minute"],
)


def create_app(config_class: Type[Config] = Config) -> Flask:
Expand All @@ -35,74 +35,25 @@ def create_app(config_class: Type[Config] = Config) -> Flask:
),
]
)

# Set content security policy
csp = {
"default-src": "'self'",
"script-src": [
"'self'",
"'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw='",
"'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='",
],
}

# Set permissions policy
permissions_policy = {
"accelerometer": "()",
"ambient-light-sensor": "()",
"autoplay": "()",
"battery": "()",
"camera": "()",
"cross-origin-isolated": "()",
"display-capture": "()",
"document-domain": "()",
"encrypted-media": "()",
"execution-while-not-rendered": "()",
"execution-while-out-of-viewport": "()",
"fullscreen": "()",
"geolocation": "()",
"gyroscope": "()",
"keyboard-map": "()",
"magnetometer": "()",
"microphone": "()",
"midi": "()",
"navigation-override": "()",
"payment": "()",
"picture-in-picture": "()",
"publickey-credentials-get": "()",
"screen-wake-lock": "()",
"sync-xhr": "()",
"usb": "()",
"web-share": "()",
"xr-spatial-tracking": "()",
"clipboard-read": "()",
"clipboard-write": "()",
"gamepad": "()",
"speaker-selection": "()",
"conversion-measurement": "()",
"focus-without-user-activation": "()",
"hid": "()",
"idle-detection": "()",
"interest-cohort": "()",
"serial": "()",
"sync-script": "()",
"trust-token-redemption": "()",
"unload": "()",
"window-management": "()",
"vertical-scroll": "()",
}
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)

# Initialise app extensions
assets.init_app(app)
compress.init_app(app)
csrf.init_app(app)
limiter.init_app(app)
talisman.init_app(app, content_security_policy=csp, permissions_policy=permissions_policy)
WTFormsHelpers(app)

# Create static asset bundles
css = Bundle("src/css/*.css", filters="cssmin", output="dist/css/custom-%(version)s.min.css")
js = Bundle("src/js/*.js", filters="jsmin", output="dist/js/custom-%(version)s.min.js")
css = Bundle(
"src/css/*.css",
filters="cssmin",
output="dist/css/custom-%(version)s.min.css",
)
js = Bundle(
"src/js/*.js",
filters="jsmin",
output="dist/js/custom-%(version)s.min.js",
)
if "css" not in assets:
assets.register("css", css)
if "js" not in assets:
Expand Down
Loading

0 comments on commit a563ee4

Please sign in to comment.