synchronize git histories to allow pushing all commits in the future

This commit is contained in:
Patrick Pirker 2023-08-18 11:03:00 +02:00
parent 3fe5f7f4fb
commit 599f396cb0
112 changed files with 5269 additions and 3055 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ app.env
**/app.env
.DS_Store
.scannerwork
.vscode

208
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,208 @@
stages:
- build-test
- test
- build-release
- release
- docs
image: docker:23.0.1
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
CI_REGISTRY_IMAGE: registry.internal.syslifters.com/reportcreator/reportcreator
services:
- docker:23.0.1-dind
.depends_docker:
before_script:
- i=0; while [ "$i" -lt 12 ]; do docker info && break; sleep 5; i=$(( i + 1 )) ; done
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD registry.internal.syslifters.com
- export DOCKER_BUILDKIT=1
build-test-api:
stage: build-test
extends: .depends_docker
script:
# Build container
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --target=api-test -t $CI_REGISTRY_IMAGE/api-test:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE/api-test:$CI_COMMIT_SHORT_SHA
test-api:
stage: test
needs: [build-test-api]
extends: .depends_docker
services:
- docker:20.10.16-dind
- postgres:14
artifacts:
when: always
paths:
- api/test-reports/junit.xml
- api/test-reports/coverage.xml
reports:
junit: api/test-reports/junit.xml
coverage_report:
coverage_format: cobertura
path: api/test-reports/coverage.xml
coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' # Regex to match coverage report
variables:
POSTGRES_DB: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
script:
- mkdir api/test-reports
- docker run -e DATABASE_HOST=${POSTGRES_PORT_5432_TCP_ADDR} -e DATABASE_NAME=${POSTGRES_DB} -e DATABASE_USER=${POSTGRES_USER} -e DATABASE_PASSWORD=${POSTGRES_PASSWORD} --mount=type=bind,source=$PWD/api/test-reports,target=/app/api/test-reports $CI_REGISTRY_IMAGE/api-test:$CI_COMMIT_SHORT_SHA pytest -n 8 --junitxml=test-reports/junit.xml --cov=reportcreator_api --cov-report=term --cov-report=xml:test-reports/coverage.xml
build-test-frontend:
stage: build-test
extends: .depends_docker
script:
- docker build --build-arg BUILDKIT_INLINE_CACHE=1 --target=frontend-test -t $CI_REGISTRY_IMAGE/frontend-test:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE/frontend-test:$CI_COMMIT_SHORT_SHA
test-frontend:
stage: test
needs: [build-test-frontend]
extends: .depends_docker
artifacts:
when: always
paths:
- frontend/test-reports
reports:
junit: frontend/test-reports/junit.xml
script:
- mkdir frontend/test-reports
- docker run --mount=type=bind,source=$PWD/frontend/test-reports,target=/app/frontend/test-reports $CI_REGISTRY_IMAGE/frontend-test:$CI_COMMIT_SHORT_SHA npm run test -- --ci
build-release:
stage: build-release
extends: .depends_docker
rules:
- if: $CI_COMMIT_TAG # Run this job when a tag is created
script:
# Parse version number, exit on invalid version number
- apk add python3 py3-pip
- VERSION_NUMBER_LEADING_ZEROS=$(echo "$CI_COMMIT_TAG" | sed -nr 's/^(prod|test|ltest)-([0-9]+\.[0-9]+([\.ab][0-9]+)?)$/\2/p')
- VERSION_NUMBER=$(python3 -c "from packaging.version import Version;print(Version('${VERSION_NUMBER_LEADING_ZEROS}'))")
# Ensure the version number is in the changelog for prod deployments
- if [[ $CI_COMMIT_TAG =~ '^prod-.*' ]]; then grep -qE "^## (v${VERSION_NUMBER}|v${VERSION_NUMBER_LEADING_ZEROS})" CHANGELOG.md || exit 1; fi
# Build containers
- docker pull $CI_REGISTRY_IMAGE/frontend-test:$CI_COMMIT_SHORT_SHA
- docker pull $CI_REGISTRY_IMAGE/api-test:$CI_COMMIT_SHORT_SHA
- docker build --cache-from $CI_REGISTRY_IMAGE/frontend-test:$CI_COMMIT_SHORT_SHA --cache-from $CI_REGISTRY_IMAGE/api-test:$CI_COMMIT_SHORT_SHA --build-arg VERSION=$VERSION_NUMBER --tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- cd languagetool && docker build -t $CI_REGISTRY_IMAGE/languagetool:$CI_COMMIT_SHORT_SHA . && cd ..
- docker push $CI_REGISTRY_IMAGE/languagetool:$CI_COMMIT_SHORT_SHA
release_job_docker:
stage: release
needs: [build-release]
extends: .depends_docker
rules:
- if: $CI_COMMIT_TAG # Run this job when a tag is created
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker pull $CI_REGISTRY_IMAGE/languagetool:$CI_COMMIT_SHORT_SHA
- docker image tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
- docker image tag $CI_REGISTRY_IMAGE/languagetool:$CI_COMMIT_SHORT_SHA $CI_REGISTRY_IMAGE/languagetool:$CI_COMMIT_TAG
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
- docker push $CI_REGISTRY_IMAGE/languagetool:$CI_COMMIT_TAG
release_job_release:
stage: release
needs: [release_job_docker]
image: registry.gitlab.com/gitlab-org/release-cli:latest
script:
- echo "works"
rules:
- if: $CI_COMMIT_TAG # Run this job when a tag is created
release:
tag_name: "$CI_COMMIT_TAG"
description: "$CI_COMMIT_TAG"
release_job_github:
stage: release
needs: [build-release]
extends: .depends_docker
rules:
- if: $CI_COMMIT_TAG =~ /^prod-.*/ # Run this job on prod deployments
script:
# Set version number
- apk add python3 py3-pip
- VERSION_NUMBER_LEADING_ZEROS=$(echo "$CI_COMMIT_TAG" | sed -nr 's/^(prod|test|ltest)-([0-9]+\.[0-9]+([\.ab][0-9]+)?)$/\2/p')
- VERSION_NUMBER=$(python3 -c "from packaging.version import Version;print(Version('${VERSION_NUMBER_LEADING_ZEROS}'))")
- echo "SYSREPTOR_VERSION=${VERSION_NUMBER}" > deploy/.env
# Generate api notice file
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker run -u0 $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA /app/api/generate_notice.sh
- CONTAINER_ID=$(docker ps -qa --filter "ancestor=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA")
- docker cp $CONTAINER_ID:/app/api/NOTICE api/NOTICE
# Delete unnecessary files
- rm -rf docs/docs/s docs/README.md docs/reporting_software.yml docs/wip docs/hooks.py dev .vscode api/.vscode
# Publish to github
- apk add git github-cli
- git clone https://${GITHUB_USERNAME}:${GITHUB_TOKEN}@github.com/Syslifters/sysreptor.git /tmp/sysreptor
- rm -rf /tmp/sysreptor/*
- cp -r * .gitignore .dockerignore /tmp/sysreptor
- cd /tmp/sysreptor
- git status
- git config --global user.email $GITHUB_USER_MAIL
- git config --global user.name $GITHUB_USERNAME
- git add .
- git commit -m "Publish v${VERSION_NUMBER}"
# Create a GitHub release with pre-built JS files
# Copy pre-built frontend files
- docker cp $CONTAINER_ID:/app/api/static api/src/
- docker cp $CONTAINER_ID:/app/api/frontend/index.html api/src/frontend/index.html
- sed -i "/^src\/static$/d" api/.gitignore
- sed -i "s/target:\ api/target:\ api-prebuilt/g" deploy/docker-compose.yml
# Copy pre-built rendering files
- docker cp $CONTAINER_ID:/app/rendering/dist rendering/dist
- sed -i "/^dist$/d" rendering/.gitignore
- rm -rf api/.vscode
# Create archive
- tar -czf /tmp/source-prebuilt.tar.gz -C /tmp sysreptor --exclude=sysreptor/.git
# Upload to GitHub
- git push
- gh release create "${VERSION_NUMBER}" /tmp/source-prebuilt.tar.gz --title="${VERSION_NUMBER}"
deploy-docs:
image: python:latest
stage: docs
needs: []
rules:
- if: $CI_COMMIT_TAG =~ /^prod-.*/ # Run this job on prod deployments
when: always
- if: $CI_COMMIT_TAG || $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual
allow_failure:
exit_codes: 127
script:
# build docs
- cd docs
- pip3 install -r requirements.txt
- set +e
- python3 -c 'from hooks import *; generate_software_lists()' || EXIT_CODE=$?
- set -e
- mkdocs build
- if [ $EXIT_CODE -ne 0 ]; then exit $EXIT_CODE; fi;
# deploy docs
- git clone https://${GITHUB_USERNAME}:${GITHUB_TOKEN}@github.com/Syslifters/sysreptor-docs.git ghpages
- ls -lA
- cd ghpages
- ls -lA
- git config --global user.email $GITHUB_USER_MAIL
- git config --global user.name $GITHUB_USERNAME
- shopt -u dotglob
- rm -rf *
- cp -r ../site/* .
- git add .
- git commit -m "INIT"
- git reset $(git commit-tree HEAD^{tree} -m "INIT")
- git push --force

View File

@ -1,5 +1,15 @@
# Changelog
## TBD
* UI: sticky header and searchbar in list views
* UI: increase file drop area for importing projects, designs and templates
* Configure finding sort order in design
* Allow manual ordering of findings by overriding the default sort order
* Allow ordering of enum choices in design field definition
* Search in all fields for template search
* Add shortcut for creating new findings and notes (Ctrl+J)
## v2023.114 - 2023-08-09
* Remove beta label and change versioning scheme
* Export notes as PDF

View File

@ -100,7 +100,9 @@ FROM python:3.10-slim-bookworm AS api-dev
# Add custom CA certificates
ARG CA_CERTIFICATES=""
RUN echo "${CA_CERTIFICATES}" | tee -a /usr/local/share/ca-certificates/custom-user-cert.crt && \
update-ca-certificates
update-ca-certificates && \
cat /etc/ssl/certs/* > /etc/ssl/certs/bundle.pem && \
pip config set global.cert /etc/ssl/certs/bundle.pem
# Install system dependencies required by weasyprint and chromium
RUN apt-get update && apt-get install -y --no-install-recommends \

2801
api/NOTICE

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

395
api/poetry.lock generated
View File

@ -168,17 +168,17 @@ files = [
[[package]]
name = "boto3"
version = "1.28.17"
version = "1.28.23"
description = "The AWS SDK for Python"
optional = false
python-versions = ">= 3.7"
files = [
{file = "boto3-1.28.17-py3-none-any.whl", hash = "sha256:bca0526f819e0f19c0f1e6eba3e2d1d6b6a92a45129f98c0d716e5aab6d9444b"},
{file = "boto3-1.28.17.tar.gz", hash = "sha256:90f7cfb5e1821af95b1fc084bc50e6c47fa3edc99f32de1a2591faa0c546bea7"},
{file = "boto3-1.28.23-py3-none-any.whl", hash = "sha256:807d4a4698ba9a76d5901a1663ff1943d13efbc388908f38b60f209c3511f1d6"},
{file = "boto3-1.28.23.tar.gz", hash = "sha256:839deb868d1278dd5a3f87208cfc4a8e259c95ca3cbe607cc322d435f02f63b0"},
]
[package.dependencies]
botocore = ">=1.31.17,<1.32.0"
botocore = ">=1.31.23,<1.32.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.6.0,<0.7.0"
@ -187,13 +187,13 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "botocore"
version = "1.31.17"
version = "1.31.23"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">= 3.7"
files = [
{file = "botocore-1.31.17-py3-none-any.whl", hash = "sha256:6ac34a1d34aa3750e78b77b8596617e2bab938964694d651939dba2cbde2c12b"},
{file = "botocore-1.31.17.tar.gz", hash = "sha256:396459065dba4339eb4da4ec8b4e6599728eb89b7caaceea199e26f7d824a41c"},
{file = "botocore-1.31.23-py3-none-any.whl", hash = "sha256:d0a95f74eb6bd99e8f52f16af0a430ba6cd1526744f40ffdd3fcccceeaf961c2"},
{file = "botocore-1.31.23.tar.gz", hash = "sha256:f3258feaebce48f138eb2675168c4d33cc3d99e9f45af13cb8de47bdc2b9c573"},
]
[package.dependencies]
@ -779,29 +779,29 @@ test = ["flake8", "isort", "pytest"]
[[package]]
name = "debugpy"
version = "1.6.7"
version = "1.6.7.post1"
description = "An implementation of the Debug Adapter Protocol for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "debugpy-1.6.7-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:b3e7ac809b991006ad7f857f016fa92014445085711ef111fdc3f74f66144096"},
{file = "debugpy-1.6.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e3876611d114a18aafef6383695dfc3f1217c98a9168c1aaf1a02b01ec7d8d1e"},
{file = "debugpy-1.6.7-cp310-cp310-win32.whl", hash = "sha256:33edb4afa85c098c24cc361d72ba7c21bb92f501104514d4ffec1fb36e09c01a"},
{file = "debugpy-1.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:ed6d5413474e209ba50b1a75b2d9eecf64d41e6e4501977991cdc755dc83ab0f"},
{file = "debugpy-1.6.7-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:38ed626353e7c63f4b11efad659be04c23de2b0d15efff77b60e4740ea685d07"},
{file = "debugpy-1.6.7-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:279d64c408c60431c8ee832dfd9ace7c396984fd7341fa3116aee414e7dcd88d"},
{file = "debugpy-1.6.7-cp37-cp37m-win32.whl", hash = "sha256:dbe04e7568aa69361a5b4c47b4493d5680bfa3a911d1e105fbea1b1f23f3eb45"},
{file = "debugpy-1.6.7-cp37-cp37m-win_amd64.whl", hash = "sha256:f90a2d4ad9a035cee7331c06a4cf2245e38bd7c89554fe3b616d90ab8aab89cc"},
{file = "debugpy-1.6.7-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:5224eabbbeddcf1943d4e2821876f3e5d7d383f27390b82da5d9558fd4eb30a9"},
{file = "debugpy-1.6.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae1123dff5bfe548ba1683eb972329ba6d646c3a80e6b4c06cd1b1dd0205e9b"},
{file = "debugpy-1.6.7-cp38-cp38-win32.whl", hash = "sha256:9cd10cf338e0907fdcf9eac9087faa30f150ef5445af5a545d307055141dd7a4"},
{file = "debugpy-1.6.7-cp38-cp38-win_amd64.whl", hash = "sha256:aaf6da50377ff4056c8ed470da24632b42e4087bc826845daad7af211e00faad"},
{file = "debugpy-1.6.7-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:0679b7e1e3523bd7d7869447ec67b59728675aadfc038550a63a362b63029d2c"},
{file = "debugpy-1.6.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de86029696e1b3b4d0d49076b9eba606c226e33ae312a57a46dca14ff370894d"},
{file = "debugpy-1.6.7-cp39-cp39-win32.whl", hash = "sha256:d71b31117779d9a90b745720c0eab54ae1da76d5b38c8026c654f4a066b0130a"},
{file = "debugpy-1.6.7-cp39-cp39-win_amd64.whl", hash = "sha256:c0ff93ae90a03b06d85b2c529eca51ab15457868a377c4cc40a23ab0e4e552a3"},
{file = "debugpy-1.6.7-py2.py3-none-any.whl", hash = "sha256:53f7a456bc50706a0eaabecf2d3ce44c4d5010e46dfc65b6b81a518b42866267"},
{file = "debugpy-1.6.7.zip", hash = "sha256:c4c2f0810fa25323abfdfa36cbbbb24e5c3b1a42cb762782de64439c575d67f2"},
{file = "debugpy-1.6.7.post1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:903bd61d5eb433b6c25b48eae5e23821d4c1a19e25c9610205f5aeaccae64e32"},
{file = "debugpy-1.6.7.post1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d16882030860081e7dd5aa619f30dec3c2f9a421e69861125f83cc372c94e57d"},
{file = "debugpy-1.6.7.post1-cp310-cp310-win32.whl", hash = "sha256:eea8d8cfb9965ac41b99a61f8e755a8f50e9a20330938ad8271530210f54e09c"},
{file = "debugpy-1.6.7.post1-cp310-cp310-win_amd64.whl", hash = "sha256:85969d864c45f70c3996067cfa76a319bae749b04171f2cdeceebe4add316155"},
{file = "debugpy-1.6.7.post1-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:890f7ab9a683886a0f185786ffbda3b46495c4b929dab083b8c79d6825832a52"},
{file = "debugpy-1.6.7.post1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4ac7a4dba28801d184b7fc0e024da2635ca87d8b0a825c6087bb5168e3c0d28"},
{file = "debugpy-1.6.7.post1-cp37-cp37m-win32.whl", hash = "sha256:3370ef1b9951d15799ef7af41f8174194f3482ee689988379763ef61a5456426"},
{file = "debugpy-1.6.7.post1-cp37-cp37m-win_amd64.whl", hash = "sha256:65b28435a17cba4c09e739621173ff90c515f7b9e8ea469b92e3c28ef8e5cdfb"},
{file = "debugpy-1.6.7.post1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:92b6dae8bfbd497c90596bbb69089acf7954164aea3228a99d7e43e5267f5b36"},
{file = "debugpy-1.6.7.post1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72f5d2ecead8125cf669e62784ef1e6300f4067b0f14d9f95ee00ae06fc7c4f7"},
{file = "debugpy-1.6.7.post1-cp38-cp38-win32.whl", hash = "sha256:f0851403030f3975d6e2eaa4abf73232ab90b98f041e3c09ba33be2beda43fcf"},
{file = "debugpy-1.6.7.post1-cp38-cp38-win_amd64.whl", hash = "sha256:3de5d0f97c425dc49bce4293df6a04494309eedadd2b52c22e58d95107e178d9"},
{file = "debugpy-1.6.7.post1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:38651c3639a4e8bbf0ca7e52d799f6abd07d622a193c406be375da4d510d968d"},
{file = "debugpy-1.6.7.post1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:038c51268367c9c935905a90b1c2d2dbfe304037c27ba9d19fe7409f8cdc710c"},
{file = "debugpy-1.6.7.post1-cp39-cp39-win32.whl", hash = "sha256:4b9eba71c290852f959d2cf8a03af28afd3ca639ad374d393d53d367f7f685b2"},
{file = "debugpy-1.6.7.post1-cp39-cp39-win_amd64.whl", hash = "sha256:973a97ed3b434eab0f792719a484566c35328196540676685c975651266fccf9"},
{file = "debugpy-1.6.7.post1-py2.py3-none-any.whl", hash = "sha256:1093a5c541af079c13ac8c70ab8b24d1d35c8cacb676306cf11e57f699c02926"},
{file = "debugpy-1.6.7.post1.zip", hash = "sha256:fe87ec0182ef624855d05e6ed7e0b7cb1359d2ffa2a925f8ec2d22e98b75d0ca"},
]
[[package]]
@ -1082,45 +1082,45 @@ pcsc = ["pyscard (>=1.9,<3)"]
[[package]]
name = "fonttools"
version = "4.41.1"
version = "4.42.0"
description = "Tools to manipulate font files"
optional = false
python-versions = ">=3.8"
files = [
{file = "fonttools-4.41.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a7bbb290d13c6dd718ec2c3db46fe6c5f6811e7ea1e07f145fd8468176398224"},
{file = "fonttools-4.41.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ec453a45778524f925a8f20fd26a3326f398bfc55d534e37bab470c5e415caa1"},
{file = "fonttools-4.41.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2071267deaa6d93cb16288613419679c77220543551cbe61da02c93d92df72f"},
{file = "fonttools-4.41.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e3334d51f0e37e2c6056e67141b2adabc92613a968797e2571ca8a03bd64773"},
{file = "fonttools-4.41.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cac73bbef7734e78c60949da11c4903ee5837168e58772371bd42a75872f4f82"},
{file = "fonttools-4.41.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:edee0900cf0eedb29d17c7876102d6e5a91ee333882b1f5abc83e85b934cadb5"},
{file = "fonttools-4.41.1-cp310-cp310-win32.whl", hash = "sha256:2a22b2c425c698dcd5d6b0ff0b566e8e9663172118db6fd5f1941f9b8063da9b"},
{file = "fonttools-4.41.1-cp310-cp310-win_amd64.whl", hash = "sha256:547ab36a799dded58a46fa647266c24d0ed43a66028cd1cd4370b246ad426cac"},
{file = "fonttools-4.41.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:849ec722bbf7d3501a0e879e57dec1fc54919d31bff3f690af30bb87970f9784"},
{file = "fonttools-4.41.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:38cdecd8f1fd4bf4daae7fed1b3170dfc1b523388d6664b2204b351820aa78a7"},
{file = "fonttools-4.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ae64303ba670f8959fdaaa30ba0c2dabe75364fdec1caeee596c45d51ca3425"},
{file = "fonttools-4.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f14f3ccea4cc7dd1b277385adf3c3bf18f9860f87eab9c2fb650b0af16800f55"},
{file = "fonttools-4.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:33191f062549e6bb1a4782c22a04ebd37009c09360e2d6686ac5083774d06d95"},
{file = "fonttools-4.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:704bccd69b0abb6fab9f5e4d2b75896afa48b427caa2c7988792a2ffce35b441"},
{file = "fonttools-4.41.1-cp311-cp311-win32.whl", hash = "sha256:4edc795533421e98f60acee7d28fc8d941ff5ac10f44668c9c3635ad72ae9045"},
{file = "fonttools-4.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:aaaef294d8e411f0ecb778a0aefd11bb5884c9b8333cc1011bdaf3b58ca4bd75"},
{file = "fonttools-4.41.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:3d1f9471134affc1e3b1b806db6e3e2ad3fa99439e332f1881a474c825101096"},
{file = "fonttools-4.41.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:59eba8b2e749a1de85760da22333f3d17c42b66e03758855a12a2a542723c6e7"},
{file = "fonttools-4.41.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9b3cc10dc9e0834b6665fd63ae0c6964c6bc3d7166e9bc84772e0edd09f9fa2"},
{file = "fonttools-4.41.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2c2964bdc827ba6b8a91dc6de792620be4da3922c4cf0599f36a488c07e2b2"},
{file = "fonttools-4.41.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7763316111df7b5165529f4183a334aa24c13cdb5375ffa1dc8ce309c8bf4e5c"},
{file = "fonttools-4.41.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b2d1ee95be42b80d1f002d1ee0a51d7a435ea90d36f1a5ae331be9962ee5a3f1"},
{file = "fonttools-4.41.1-cp38-cp38-win32.whl", hash = "sha256:f48602c0b3fd79cd83a34c40af565fe6db7ac9085c8823b552e6e751e3a5b8be"},
{file = "fonttools-4.41.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0938ebbeccf7c80bb9a15e31645cf831572c3a33d5cc69abe436e7000c61b14"},
{file = "fonttools-4.41.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e5c2b0a95a221838991e2f0e455dec1ca3a8cc9cd54febd68cc64d40fdb83669"},
{file = "fonttools-4.41.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:891cfc5a83b0307688f78b9bb446f03a7a1ad981690ac8362f50518bc6153975"},
{file = "fonttools-4.41.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73ef0bb5d60eb02ba4d3a7d23ada32184bd86007cb2de3657cfcb1175325fc83"},
{file = "fonttools-4.41.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f240d9adf0583ac8fc1646afe7f4ac039022b6f8fa4f1575a2cfa53675360b69"},
{file = "fonttools-4.41.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bdd729744ae7ecd7f7311ad25d99da4999003dcfe43b436cf3c333d4e68de73d"},
{file = "fonttools-4.41.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b927e5f466d99c03e6e20961946314b81d6e3490d95865ef88061144d9f62e38"},
{file = "fonttools-4.41.1-cp39-cp39-win32.whl", hash = "sha256:afce2aeb80be72b4da7dd114f10f04873ff512793d13ce0b19d12b2a4c44c0f0"},
{file = "fonttools-4.41.1-cp39-cp39-win_amd64.whl", hash = "sha256:1df1b6f4c7c4bc8201eb47f3b268adbf2539943aa43c400f84556557e3e109c0"},
{file = "fonttools-4.41.1-py3-none-any.whl", hash = "sha256:952cb405f78734cf6466252fec42e206450d1a6715746013f64df9cbd4f896fa"},
{file = "fonttools-4.41.1.tar.gz", hash = "sha256:e16a9449f21a93909c5be2f5ed5246420f2316e94195dbfccb5238aaa38f9751"},
{file = "fonttools-4.42.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9c456d1f23deff64ffc8b5b098718e149279abdea4d8692dba69172fb6a0d597"},
{file = "fonttools-4.42.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:150122ed93127a26bc3670ebab7e2add1e0983d30927733aec327ebf4255b072"},
{file = "fonttools-4.42.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48e82d776d2e93f88ca56567509d102266e7ab2fb707a0326f032fe657335238"},
{file = "fonttools-4.42.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58c1165f9b2662645de9b19a8c8bdd636b36294ccc07e1b0163856b74f10bafc"},
{file = "fonttools-4.42.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d6dc3fa91414ff4daa195c05f946e6a575bd214821e26d17ca50f74b35b0fe4"},
{file = "fonttools-4.42.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fae4e801b774cc62cecf4a57b1eae4097903fced00c608d9e2bc8f84cd87b54a"},
{file = "fonttools-4.42.0-cp310-cp310-win32.whl", hash = "sha256:b8600ae7dce6ec3ddfb201abb98c9d53abbf8064d7ac0c8a0d8925e722ccf2a0"},
{file = "fonttools-4.42.0-cp310-cp310-win_amd64.whl", hash = "sha256:57b68eab183fafac7cd7d464a7bfa0fcd4edf6c67837d14fb09c1c20516cf20b"},
{file = "fonttools-4.42.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0a1466713e54bdbf5521f2f73eebfe727a528905ff5ec63cda40961b4b1eea95"},
{file = "fonttools-4.42.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3fb2a69870bfe143ec20b039a1c8009e149dd7780dd89554cc8a11f79e5de86b"},
{file = "fonttools-4.42.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae881e484702efdb6cf756462622de81d4414c454edfd950b137e9a7352b3cb9"},
{file = "fonttools-4.42.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27ec3246a088555629f9f0902f7412220c67340553ca91eb540cf247aacb1983"},
{file = "fonttools-4.42.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8ece1886d12bb36c48c00b2031518877f41abae317e3a55620d38e307d799b7e"},
{file = "fonttools-4.42.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:10dac980f2b975ef74532e2a94bb00e97a95b4595fb7f98db493c474d5f54d0e"},
{file = "fonttools-4.42.0-cp311-cp311-win32.whl", hash = "sha256:83b98be5d291e08501bd4fc0c4e0f8e6e05b99f3924068b17c5c9972af6fff84"},
{file = "fonttools-4.42.0-cp311-cp311-win_amd64.whl", hash = "sha256:e35bed436726194c5e6e094fdfb423fb7afaa0211199f9d245e59e11118c576c"},
{file = "fonttools-4.42.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c36c904ce0322df01e590ba814d5d69e084e985d7e4c2869378671d79662a7d4"},
{file = "fonttools-4.42.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d54e600a2bcfa5cdaa860237765c01804a03b08404d6affcd92942fa7315ffba"},
{file = "fonttools-4.42.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01cfe02416b6d416c5c8d15e30315cbcd3e97d1b50d3b34b0ce59f742ef55258"},
{file = "fonttools-4.42.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f81ed9065b4bd3f4f3ce8e4873cd6a6b3f4e92b1eddefde35d332c6f414acc3"},
{file = "fonttools-4.42.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:685a4dd6cf31593b50d6d441feb7781a4a7ef61e19551463e14ed7c527b86f9f"},
{file = "fonttools-4.42.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:329341ba3d86a36e482610db56b30705384cb23bd595eac8cbb045f627778e9d"},
{file = "fonttools-4.42.0-cp38-cp38-win32.whl", hash = "sha256:4655c480a1a4d706152ff54f20e20cf7609084016f1df3851cce67cef768f40a"},
{file = "fonttools-4.42.0-cp38-cp38-win_amd64.whl", hash = "sha256:6bd7e4777bff1dcb7c4eff4786998422770f3bfbef8be401c5332895517ba3fa"},
{file = "fonttools-4.42.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9b55d2a3b360e0c7fc5bd8badf1503ca1c11dd3a1cd20f2c26787ffa145a9c7"},
{file = "fonttools-4.42.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0df8ef75ba5791e873c9eac2262196497525e3f07699a2576d3ab9ddf41cb619"},
{file = "fonttools-4.42.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd2363ea7728496827658682d049ffb2e98525e2247ca64554864a8cc945568"},
{file = "fonttools-4.42.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40673b2e927f7cd0819c6f04489dfbeb337b4a7b10fc633c89bf4f34ecb9620"},
{file = "fonttools-4.42.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c8bf88f9e3ce347c716921804ef3a8330cb128284eb6c0b6c4b3574f3c580023"},
{file = "fonttools-4.42.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:703101eb0490fae32baf385385d47787b73d9ea55253df43b487c89ec767e0d7"},
{file = "fonttools-4.42.0-cp39-cp39-win32.whl", hash = "sha256:f0290ea7f9945174bd4dfd66e96149037441eb2008f3649094f056201d99e293"},
{file = "fonttools-4.42.0-cp39-cp39-win_amd64.whl", hash = "sha256:ae7df0ae9ee2f3f7676b0ff6f4ebe48ad0acaeeeaa0b6839d15dbf0709f2c5ef"},
{file = "fonttools-4.42.0-py3-none-any.whl", hash = "sha256:dfe7fa7e607f7e8b58d0c32501a3a7cac148538300626d1b930082c90ae7f6bd"},
{file = "fonttools-4.42.0.tar.gz", hash = "sha256:614b1283dca88effd20ee48160518e6de275ce9b5456a3134d5f235523fc5065"},
]
[package.dependencies]
@ -1269,13 +1269,13 @@ lxml = ["lxml"]
[[package]]
name = "httpcore"
version = "0.16.3"
version = "0.17.3"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.7"
files = [
{file = "httpcore-0.16.3-py3-none-any.whl", hash = "sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0"},
{file = "httpcore-0.16.3.tar.gz", hash = "sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb"},
{file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"},
{file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"},
]
[package.dependencies]
@ -1290,24 +1290,24 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "httpx"
version = "0.23.3"
version = "0.24.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.7"
files = [
{file = "httpx-0.23.3-py3-none-any.whl", hash = "sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6"},
{file = "httpx-0.23.3.tar.gz", hash = "sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9"},
{file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"},
{file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"},
]
[package.dependencies]
certifi = "*"
httpcore = ">=0.15.0,<0.17.0"
rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]}
httpcore = ">=0.15.0,<0.18.0"
idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<13)"]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
@ -1357,13 +1357,13 @@ files = [
[[package]]
name = "jsonschema"
version = "4.18.4"
version = "4.19.0"
description = "An implementation of JSON Schema validation for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "jsonschema-4.18.4-py3-none-any.whl", hash = "sha256:971be834317c22daaa9132340a51c01b50910724082c2c1a2ac87eeec153a3fe"},
{file = "jsonschema-4.18.4.tar.gz", hash = "sha256:fb3642735399fa958c0d2aad7057901554596c63349f4f6b283c493cf692a25d"},
{file = "jsonschema-4.19.0-py3-none-any.whl", hash = "sha256:043dc26a3845ff09d20e4420d6012a9c91c9aa8999fa184e7efcfeccb41e32cb"},
{file = "jsonschema-4.19.0.tar.gz", hash = "sha256:6e1e7569ac13be8139b2dd2c21a55d350066ee3f80df06c608b398cdc6f30e8f"},
]
[package.dependencies]
@ -1575,13 +1575,13 @@ files = [
[[package]]
name = "phonenumberslite"
version = "8.13.17"
version = "8.13.18"
description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
optional = false
python-versions = "*"
files = [
{file = "phonenumberslite-8.13.17-py2.py3-none-any.whl", hash = "sha256:bae91ba7822ed73adeac739b9f9f2ded295375542014f3374e593ad92eef49c4"},
{file = "phonenumberslite-8.13.17.tar.gz", hash = "sha256:5741de4b77a963f33585eb0e8ffa2632ea9987d6e50a38ac67f441e49422de69"},
{file = "phonenumberslite-8.13.18-py2.py3-none-any.whl", hash = "sha256:40cef03b24f2bc5711fed2b53b72770ff58f6b7dbfff749822c91078d6e82481"},
{file = "phonenumberslite-8.13.18.tar.gz", hash = "sha256:a321f0decf3e4e080f005fda3fba5a791d9d14a3ca217974345ff452923c31e2"},
]
[[package]]
@ -1704,74 +1704,6 @@ files = [
docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
[[package]]
name = "pillow-heif"
version = "0.10.1"
description = "Python interface for libheif library"
optional = false
python-versions = ">=3.7"
files = [
{file = "pillow_heif-0.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2e34110c906035f9902bb7dee964384e33b45c4545cee0fc4f78bd06b6cffbe0"},
{file = "pillow_heif-0.10.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:9d67655cde69eb76f7b5a3f3b3069998d43c9cd157a1e41997fe165a44614401"},
{file = "pillow_heif-0.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd3b2bfa20f3af072c1a1fedbdee441b71972969e09efc6b0f9789b540d51899"},
{file = "pillow_heif-0.10.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:091e43a45b1ed155c65a3a99252ba5d1ea7ba9ba7e9880afa06997533abe4875"},
{file = "pillow_heif-0.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd01437bca86e61b252a0e730c2181b3dd3bfb57367c0473a8dca6db53be5818"},
{file = "pillow_heif-0.10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2229077a834182477cfb8f665c4c42ce9766d90d746d74c7ab6d48945c8a6992"},
{file = "pillow_heif-0.10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f62617d91e6656535fde6ddb61f413c27e81f2d58eb38201b62982a05a729acd"},
{file = "pillow_heif-0.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f98a5c77626bfb1dfdc83939fe44eb11ab721edfd4ca516e8e9b8e3c0dcfbe13"},
{file = "pillow_heif-0.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:2c791917a9e286f3d692f5c162dedf07e65ebab18c4df7ad7a5a109d395aaca9"},
{file = "pillow_heif-0.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b21d19372d9a1cc22a6e639cc929bc3abae7f701ee7c8b66bad5302f36977eef"},
{file = "pillow_heif-0.10.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c57bbb1a1aabb88efa72ba24300a3df733826ed8892d5bbcc8317b4262e95a03"},
{file = "pillow_heif-0.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d4b04bf35280f7d895ba783c4b7f7e3d0f139c99fd736e1831d2cfe06a41c10"},
{file = "pillow_heif-0.10.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2722a220d898cbcd1e3d6bcb669a28cfcb240d05f41bcd57d4b78af991b32cc"},
{file = "pillow_heif-0.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae92c3e9b348e367122b140fd7a744bdb087c551ac00efc2b486a410569d00f"},
{file = "pillow_heif-0.10.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:400b25a1110ef5dfe394255646bae5318779d2ec4c787792bd5ba72956df628f"},
{file = "pillow_heif-0.10.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:16db680b312ea684b3b88a3f97b3b122df48e12a057351c3ed1f435dd0a634d2"},
{file = "pillow_heif-0.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db7363f190faeda67b15cf774fddf6c658a5681abb8b9860dcbc47cc85d668f8"},
{file = "pillow_heif-0.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:7b84073e2997f34062751e8dd0a644e3e8f6fd952265edfe7ee021531a939018"},
{file = "pillow_heif-0.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ef1c87acea720edf784fa3da77d3292f288de1c9f40e9808f4c6837dd167afc3"},
{file = "pillow_heif-0.10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dae1ca05c818abc31bbc259a17554c3dd9faca4d79618f06f0cc2439320c4f58"},
{file = "pillow_heif-0.10.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dece6099058422ab7a66b713e9fc3ea4e21946a95442c276956825602a0782c"},
{file = "pillow_heif-0.10.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8173d2843207a1c3265e382e7dcb02d8d5f882b5cd8ab9a1701c5bf47639ae22"},
{file = "pillow_heif-0.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:0ed8652a520a46aa936b816bb3fcd445aba5ae6678f444927dcd6e7f831e02db"},
{file = "pillow_heif-0.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:da5c734c9510ccb05f42199bedb6b0f126f9e8447e3bde3ad03f3882817ad08c"},
{file = "pillow_heif-0.10.1-cp38-cp38-macosx_12_0_arm64.whl", hash = "sha256:27c1b4e388fde47f690a0b8e4299a8da57329a35e1924444028865e0efd20430"},
{file = "pillow_heif-0.10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05406e07d6640e122729e249ad6a2bf28c1aabe0dde0a71217ad54c36854e0e9"},
{file = "pillow_heif-0.10.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffa99da11b0328dc483976d5c4e62cccc75903e0bcc861e3d9fbce2752f0dff5"},
{file = "pillow_heif-0.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6f4f01006dfa5cfefd1e960763e2f3bd829e0c6e6d8202462fc3f7d0b91dfd"},
{file = "pillow_heif-0.10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ea6cf2255179bb667b75b834845083f23959fc3873c444a15f54cad415e501dd"},
{file = "pillow_heif-0.10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:bc12fc70de7f59a313678255b9abc7acd4915032cdbdb887a402f1e6c632e95d"},
{file = "pillow_heif-0.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:50cbb535e9b776bd327d7344e22bec1f7457ae587487189a136339cf90952a99"},
{file = "pillow_heif-0.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:82143407c590122e1d36bf674d7d589d20ed76fac243a65d1704e6b0fbc14dde"},
{file = "pillow_heif-0.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4bf6abce62e934e33dbd5cf8528c76c746397116a87128b913278554eb840c3b"},
{file = "pillow_heif-0.10.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:5909585d1878dfe214a7bc6ae502ce6e1ee99cab88dd0669714c2d524f8509da"},
{file = "pillow_heif-0.10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95c0e83ef5237b18ae5e4adc5e5c9261b23c13704abedf1bbb46cc44d086312a"},
{file = "pillow_heif-0.10.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:158dc0eabaadb13240d2bc14ce11047a661a4748e56423a5346c4ffa9831e0e3"},
{file = "pillow_heif-0.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:856a4f46a689bc037c0e51b8ceae1e7944907a2c8a3767dd4d72c9f781ed82b7"},
{file = "pillow_heif-0.10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:41a75fbf044db03d3e5d64c8288b7ea3ba4b9575ff1078f1df814936f15d11b7"},
{file = "pillow_heif-0.10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e9745aab7ed2bb0e53548e1e2c906721b0bc76adedeb17e661ec9ccbd8b698fd"},
{file = "pillow_heif-0.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ade9dbfbc5653fcf345fd8db75fb4fec603b521b1a832f091a809258d2232b5"},
{file = "pillow_heif-0.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:41610fae8e2494f605b7b5c2508f6c2688227a7cd3f2c71e1fff966fd9476297"},
{file = "pillow_heif-0.10.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a49c5671f74d8d58e4a0d507a3cdbd37c28693f5ad50b5bed5983a2b693e572a"},
{file = "pillow_heif-0.10.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de3a2929e509a93981866fb9ec2f313ee349312009ca50ed1ca999c4039c31e1"},
{file = "pillow_heif-0.10.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1e50cab15f2531ea5bdda9b15e5f2d05bf023b607e4322bc600dd18e3783757"},
{file = "pillow_heif-0.10.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:dc143d3f61b7a7d28f4200be9cdcf0149b5da44511d8faacb4778a9dc264e900"},
{file = "pillow_heif-0.10.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c57dc8496e59d4d9b8f79e66be148e5c898704b7bbd65531d69352bce2e820f0"},
{file = "pillow_heif-0.10.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37dd748836c8d5d82ef5395cd8aee523dba5bc0c6a77353baacf7868de41eec3"},
{file = "pillow_heif-0.10.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28a3872f66d55d74ea4c18f1460ccba1bae20874100331b58dae6bbc240c63a5"},
{file = "pillow_heif-0.10.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9c6880056df5898cada6f65b5dc6ba8259da1b570491c18da867420f32314512"},
{file = "pillow_heif-0.10.1.tar.gz", hash = "sha256:af9bd9d8fc189451edb193f321214207bf890d0ac80ac697056def39fec7565d"},
]
[package.dependencies]
pillow = ">=8.4.0"
[package.extras]
dev = ["coverage", "defusedxml", "numpy", "opencv-python (==4.7.0.72)", "packaging", "pre-commit", "pylint", "pympler", "pytest"]
docs = ["sphinx (>=4.4)", "sphinx-issues (>=3.0.1)", "sphinx-rtd-theme (>=1.0)"]
tests = ["defusedxml", "numpy", "packaging", "pympler", "pytest"]
tests-min = ["defusedxml", "packaging", "pytest"]
[[package]]
name = "playwright"
version = "1.36.0"
@ -1823,89 +1755,89 @@ wcwidth = "*"
[[package]]
name = "psycopg"
version = "3.1.9"
version = "3.1.10"
description = "PostgreSQL database adapter for Python"
optional = false
python-versions = ">=3.7"
files = [
{file = "psycopg-3.1.9-py3-none-any.whl", hash = "sha256:fbbac339274d8733ee70ba9822297af3e8871790a26e967b5ea53e30a4b74dcc"},
{file = "psycopg-3.1.9.tar.gz", hash = "sha256:ab400f207a8c120bafdd8077916d8f6c0106e809401378708485b016508c30c9"},
{file = "psycopg-3.1.10-py3-none-any.whl", hash = "sha256:8bbeddae5075c7890b2fa3e3553440376d3c5e28418335dee3c3656b06fa2b52"},
{file = "psycopg-3.1.10.tar.gz", hash = "sha256:15b25741494344c24066dc2479b0f383dd1b82fa5e75612fa4fa5bb30726e9b6"},
]
[package.dependencies]
psycopg-binary = {version = "3.1.9", optional = true, markers = "extra == \"binary\""}
psycopg-binary = {version = "3.1.10", optional = true, markers = "extra == \"binary\""}
typing-extensions = ">=4.1"
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
binary = ["psycopg-binary (==3.1.9)"]
c = ["psycopg-c (==3.1.9)"]
dev = ["black (>=23.1.0)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.2)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
binary = ["psycopg-binary (==3.1.10)"]
c = ["psycopg-c (==3.1.10)"]
dev = ["black (>=23.1.0)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"]
pool = ["psycopg-pool"]
test = ["anyio (>=3.6.2)", "mypy (>=1.2)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"]
test = ["anyio (>=3.6.2)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"]
[[package]]
name = "psycopg-binary"
version = "3.1.9"
version = "3.1.10"
description = "PostgreSQL database adapter for Python -- C optimisation distribution"
optional = false
python-versions = ">=3.7"
files = [
{file = "psycopg_binary-3.1.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:284038cbe3f5a0f3de417af9b5eaa2a9524a3a06211523cf245111c71b566506"},
{file = "psycopg_binary-3.1.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2cea4bb0b19245c83486868d7c66f73238c4caa266b5b3c3d664d10dab2ab56"},
{file = "psycopg_binary-3.1.9-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe5c5c31f59ccb1d1f473466baa93d800138186286e80e251f930e49c80d208"},
{file = "psycopg_binary-3.1.9-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82704a899d57c29beba5399d41eab5ef5c238b810d7e25e2d1916d2b34c4b1a3"},
{file = "psycopg_binary-3.1.9-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eab449e39db1c429cac79b7aa27e6827aad4995f32137e922db7254f43fed7b5"},
{file = "psycopg_binary-3.1.9-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87e0c97733b11eeca3d24e56df70f3f9d792b2abd46f48be2fb2348ffc3e7e39"},
{file = "psycopg_binary-3.1.9-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:81e34d6df54329424944d5ca91b1cc77df6b8a9130cb5480680d56f53d4e485c"},
{file = "psycopg_binary-3.1.9-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e2f463079d99568a343ed0b766150b30627e9ed41de99fd82e945e7e2bec764a"},
{file = "psycopg_binary-3.1.9-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f2cbdef6568da21c39dfd45c2074e85eabbd00e1b721832ba94980f01f582dd4"},
{file = "psycopg_binary-3.1.9-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53afb0cc2ebe74651f339e22d05ec082a0f44939715d9138d357852f074fcf55"},
{file = "psycopg_binary-3.1.9-cp310-cp310-win_amd64.whl", hash = "sha256:09167f106e7685591b4cdf58eff0191fb7435d586f384133a0dd30df646cf409"},
{file = "psycopg_binary-3.1.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a8aaa47c1791fc05c0229ec1003dd49e13238fba9434e1fc3b879632f749c3c4"},
{file = "psycopg_binary-3.1.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3d91ee0d33ac7b42d0488a9be2516efa2ec00901b81d69566ff34a7a94b66c0b"},
{file = "psycopg_binary-3.1.9-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5e36504373e5bcdc954b1da1c6fe66379007fe1e329790e8fb72b879a01e097"},
{file = "psycopg_binary-3.1.9-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c1def6c2d28e257325b3b208cf1966343b498282a0f4d390fda7b7e0577da64"},
{file = "psycopg_binary-3.1.9-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055537a9c20efe9bf17cb72bd879602eda71de6f737ebafa1953e017c6a37fbe"},
{file = "psycopg_binary-3.1.9-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b164355d023a91b23dcc4bb3112bc7d6e9b9c938fb5abcb6e54457d2da1f317"},
{file = "psycopg_binary-3.1.9-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03b08545ce1c627f4d5e6384eda2946660c4ba6ceb0a09ae47de07419f725669"},
{file = "psycopg_binary-3.1.9-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1e31bac3d2d41e6446b20b591f638943328c958f4d1ce13d6f1c5db97c3a8dee"},
{file = "psycopg_binary-3.1.9-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a274c63c8fb9d419509bed2ef72befc1fd04243972e17e7f5afc5725cb13a560"},
{file = "psycopg_binary-3.1.9-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:98d9d156b9ada08c271a79662fc5fcc1731b4d7c1f651ef5843d818d35f15ba0"},
{file = "psycopg_binary-3.1.9-cp311-cp311-win_amd64.whl", hash = "sha256:c3a13aa022853891cadbc7256a9804e5989def760115c82334bddf0d19783b0b"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1a321ef3579a8de0545ade6ff1edfde0c88b8847d58c5615c03751c76054796"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5833bda4c14f24c6a8ac08d3c5712acaa4f35aab31f9ccd2265e9e9a7d0151c8"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a207d5a7f4212443b7452851c9ccd88df9c6d4d58fa2cea2ead4dd9cb328e578"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:07414daa86662f7657e9fabe49af85a32a975e92e6568337887d9c9ffedc224f"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17c5d4936c746f5125c6ef9eb43655e27d4d0c9ffe34c3073878b43c3192511d"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5cdc13c8ec1437240801e43d07e27ff6479ac9dd8583ecf647345bfd2e8390e4"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3836bdaf030a5648bd5f5b452e4b068b265e28f9199060c5b70dbf4a218cde6e"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:96725d9691a84a21eb3e81c884a2e043054e33e176801a57a05e9ac38d142c6e"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dade344aa90bb0b57d1cfc13304ed83ab9a36614b8ddd671381b2de72fe1483d"},
{file = "psycopg_binary-3.1.9-cp37-cp37m-win_amd64.whl", hash = "sha256:db866cc557d9761036771d666d17fa4176c537af7e6098f42a6bf8f64217935f"},
{file = "psycopg_binary-3.1.9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3b62545cc64dd69ea0ae5ffe18d7c97e03660ab8244aa8c5172668a21c41daa0"},
{file = "psycopg_binary-3.1.9-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:058ab0d79be0b229338f0e61fec6f475077518cba63c22c593645a69f01c3e23"},
{file = "psycopg_binary-3.1.9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2340ca2531f69e5ebd9d18987362ba57ed6ab6a271511d8026814a46a2a87b59"},
{file = "psycopg_binary-3.1.9-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b816ce0e27a2a8786d34b61d3e36e01029245025879d64b88554326b794a4f0"},
{file = "psycopg_binary-3.1.9-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b36fe4314a784fbe45c9fd71c902b9bf57341aff9b97c0cbd22f8409a271e2f"},
{file = "psycopg_binary-3.1.9-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b246fed629482b06f938b23e9281c4af592329daa3ec2cd4a6841ccbfdeb4d68"},
{file = "psycopg_binary-3.1.9-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:90787ac05b932c0fc678cbf470ccea9c385b8077583f0490136b4569ed3fb652"},
{file = "psycopg_binary-3.1.9-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9c114f678e8f4a96530fa79cfd84f65f26358ecfc6cca70cfa2d5e3ae5ef217a"},
{file = "psycopg_binary-3.1.9-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3a82e77400d1ef6c5bbcf3e600e8bdfacf1a554512f96c090c43ceca3d1ce3b6"},
{file = "psycopg_binary-3.1.9-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c7d990f14a37345ca05a5192cd5ac938c9cbedca9c929872af6ae311158feb0e"},
{file = "psycopg_binary-3.1.9-cp38-cp38-win_amd64.whl", hash = "sha256:e0ca74fd85718723bb9f08e0c6898e901a0c365aef20b3c3a4ef8709125d6210"},
{file = "psycopg_binary-3.1.9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ce8f4dea5934aa6c4933e559c74bef4beb3413f51fbcf17f306ce890216ac33a"},
{file = "psycopg_binary-3.1.9-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f41a9e0de4db194c053bcc7c00c35422a4d19d92a8187e8065b1c560626efe35"},
{file = "psycopg_binary-3.1.9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f94a7985135e084e122b143956c6f589d17aef743ecd0a434a3d3a222631d5a"},
{file = "psycopg_binary-3.1.9-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bb86d58b90faefdc0bbedf08fdea4cc2afcb1cfa4340f027d458bfd01d8b812"},
{file = "psycopg_binary-3.1.9-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c696dc84f9ff155761df15779181d8e4af7746b98908e130add8259912e4bb7"},
{file = "psycopg_binary-3.1.9-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4213953da44324850c8f789301cf665f46fb94301ba403301e7af58546c3a428"},
{file = "psycopg_binary-3.1.9-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:25e3ce947aaaa1bd9f1920fca76d7281660646304f9ea5bc036b201dd8790655"},
{file = "psycopg_binary-3.1.9-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9c75be2a9b986139e3ff6bc0a2852081ac00811040f9b82d3aa539821311122e"},
{file = "psycopg_binary-3.1.9-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:63e8d1dbe253657c70dbfa9c59423f4654d82698fc5ed6868b8dc0765abe20b6"},
{file = "psycopg_binary-3.1.9-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f4da4ca9b2365fc1d3fc741c3bbd3efccd892ce813444b884c8911a1acf1c932"},
{file = "psycopg_binary-3.1.9-cp39-cp39-win_amd64.whl", hash = "sha256:c0b8d6bbeff1dba760a208d8bc205a05b745e6cee02b839f969f72cf56a8b80d"},
{file = "psycopg_binary-3.1.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a529c203f6e0f4c67ba27cf8f9739eb3bc880ad70d6ad6c0e56c2230a66b5a09"},
{file = "psycopg_binary-3.1.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd6e14d1aeb12754a43446c77a5ce819b68875cc25ae6538089ef90d7f6dd6f7"},
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1583ced5948cf88124212c4503dfe5b01ac3e2dd1a2833c083917f4c4aabe8b4"},
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2098721c486478987be700723b28ec7a48f134eba339de36af0e745f37dfe461"},
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e61f7b412fca7b15dd043a0b22fd528d2ed8276e76b3764c3889e29fa65082b"},
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0f33e33a072e3d5af51ee4d4a439e10dbe623fe87ef295d5d688180d529f13f"},
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f6f7738c59262d8d19154164d99c881ed58ed377fb6f1d685eb0dc43bbcd8022"},
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:511d38b1e1961d179d47d5103ba9634ecfc7ead431d19a9337ef82f3a2bca807"},
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:666e7acf2ffdb5e8a58e8b0c1759facdb9688c7e90ee8ca7aed675803b57404d"},
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:57b93c756fee5f7c7bd580c34cd5d244f7d5638f8b2cf25333f97b9b8b2ebfd1"},
{file = "psycopg_binary-3.1.10-cp310-cp310-win_amd64.whl", hash = "sha256:a1d61b7724c7215a8ea4495a5c6b704656f4b7bb6165f4cb9989b685886ebc48"},
{file = "psycopg_binary-3.1.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:36fff836a7823c9d71fa7faa333c74b2b081af216cebdbb0f481dce55ee2d974"},
{file = "psycopg_binary-3.1.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:32caf98cb00881bfcbbbae39a15f2a4e08b79ff983f1c0f13b60a888ef6e8431"},
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5565a6a86fee8d74f30de89e07f399567cdf59367aeb09624eb690d524339076"},
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fb0d64520b29bd80a6731476ad8e1c20348dfdee00ab098899d23247b641675"},
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfc05ed4e74fa8615d7cc2bd57f00f97662f4e865a731dbd43da9a527e289c8c"},
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5b59c8cff887757ddf438ff9489d79c5e6b717112c96f5c68e16f367ff8724e"},
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbaf12361136afefc5faab21a174a437e71c803b083f410e5140c7605bc66b"},
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ff72576061c774bcce5f5440b93e63d4c430032dd056d30f6cb1988e549dd92c"},
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a4e91e1a8d61c60f592a1dfcebdf55e52a29fe4fdb650c5bd5414c848e77d029"},
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f7187269d825e84c945be7d93dd5088a4e0b6481a4bdaba3bf7069d4ac13703d"},
{file = "psycopg_binary-3.1.10-cp311-cp311-win_amd64.whl", hash = "sha256:ba7812a593c16d9d661844dc8dd4d81548fd1c2a0ee676f3e3d8638369f4c5e4"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88caa5859740507b3596c6c2e00ceaccee2c6ab5317bc535887801ad3cc7f3e1"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a3a7e99ba10c2e83a48d79431560e0d5ca7865f68f2bac3a462dc2b151e9926"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:848f4f4707dc73f4b4e844c92f3de795b2ddb728f75132602bda5e6ba55084fc"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:415961e839bb49cfd75cd961503fb8846c0768f247db1fa7171c1ac61d38711b"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0471869e658d0c6b8c3ed53153794739c18d7dad2dd5b8e6ff023a364c20f7df"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4290060ee0d856caa979ecf675c0e6959325f508272ccf27f64c3801c7bcbde7"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:abf04bc06c8f6a1ac3dc2106d3b79c8661352e9d8a57ca2934ffa6aae8fe600a"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:51fe70708243b83bf16710d8c11b61bd46562e6a24a6300d5434380b35911059"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b658f7f8b49fb60a1c52e3f6692f690a85bdf1ad30aafe0f3f1fd74f6958cf8"},
{file = "psycopg_binary-3.1.10-cp37-cp37m-win_amd64.whl", hash = "sha256:ffc8c796194f23b9b07f6d25f927ec4df84a194bbc7a1f9e73316734eef512f9"},
{file = "psycopg_binary-3.1.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:74ce92122be34cf0e5f06d79869e1001c8421a68fa7ddf6fe38a717155cf3a64"},
{file = "psycopg_binary-3.1.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:75608a900984061c8898be68fbddc6f3da5eefdffce6e0624f5371645740d172"},
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6670d160d054466e8fdedfbc749ef8bf7dfdf69296048954d24645dd4d3d3c01"},
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d32026cfab7ba7ac687a42c33345026a2fb6fc5608a6144077f767af4386be0b"},
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:908fa388a5b75dfd17a937acb24708bd272e21edefca9a495004c6f70ec2636a"},
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e46b97073bd4de114f475249d681eaf054e950699c5d7af554d3684db39b82d"},
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9cf56bb4b115def3a18157f3b3b7d8322ee94a8dea30028db602c8f9ae34ad1e"},
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3b6c6f90241c4c5a6ca3f0d8827e37ef90fdc4deb9d8cfa5678baa0ea374b391"},
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:747176a6aeb058079f56c5397bd90339581ab7b3cc0d62e7445654e6a484c7e1"},
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41a415e78c457b06497fa0084e4ea7245ca1a377b55756dd757034210b64da7e"},
{file = "psycopg_binary-3.1.10-cp38-cp38-win_amd64.whl", hash = "sha256:a7bbe9017edd898d7b3a8747700ed045dda96a907dff87f45e642e28d8584481"},
{file = "psycopg_binary-3.1.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f062f20256708929a58c41d44f350efced4c00a603323d1413f6dc0b84d95a5"},
{file = "psycopg_binary-3.1.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dea30f2704337ca2d0322fccfe1fa30f61ce9185de3937eb986321063114a51f"},
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9d88ac72531034ebf7ec09114e732b066a9078f4ce213cf65cc5e42eb538d30"},
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2bea0940d69c3e24a72530730952687912893b34c53aa39e79045e7b446174d"},
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a691dc8e2436d9c1e5cf93902d63e9501688fccc957eb22f952d37886257470"},
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa92661f99351765673835a4d936d79bd24dfbb358b29b084d83be38229a90e4"},
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:30eb731ed5525d8df892db6532cc8ffd8a163b73bc355127dee9c49334e16eee"},
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:50bf7a59d3a85a82d466fed341d352b44d09d6adc18656101d163a7cfc6509a0"},
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f48665947c55f8d6eb3f0be98de80411508e1ec329f354685329b57fced82c7f"},
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:caa771569da01fc0389ca34920c331a284425a68f92d1ba0a80cc08935f8356e"},
{file = "psycopg_binary-3.1.10-cp39-cp39-win_amd64.whl", hash = "sha256:b30887e631fd67affaed98f6cd2135b44f2d1a6d9bca353a69c3889c78bd7aa8"},
]
[[package]]
@ -2229,13 +2161,13 @@ test = ["coverage", "pytest"]
[[package]]
name = "referencing"
version = "0.30.0"
version = "0.30.2"
description = "JSON Referencing + Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "referencing-0.30.0-py3-none-any.whl", hash = "sha256:c257b08a399b6c2f5a3510a50d28ab5dbc7bbde049bcaf954d43c446f83ab548"},
{file = "referencing-0.30.0.tar.gz", hash = "sha256:47237742e990457f7512c7d27486394a9aadaf876cbfaa4be65b27b4f4d47c6b"},
{file = "referencing-0.30.2-py3-none-any.whl", hash = "sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf"},
{file = "referencing-0.30.2.tar.gz", hash = "sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0"},
]
[package.dependencies]
@ -2263,23 +2195,6 @@ urllib3 = ">=1.21.1,<3"
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rfc3986"
version = "1.5.0"
description = "Validating URI References per RFC 3986"
optional = false
python-versions = "*"
files = [
{file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"},
{file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"},
]
[package.dependencies]
idna = {version = "*", optional = true, markers = "extra == \"idna2008\""}
[package.extras]
idna2008 = ["idna"]
[[package]]
name = "rpds-py"
version = "0.9.2"
@ -2537,18 +2452,19 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
[[package]]
name = "uvicorn"
version = "0.21.1"
version = "0.23.2"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "uvicorn-0.21.1-py3-none-any.whl", hash = "sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742"},
{file = "uvicorn-0.21.1.tar.gz", hash = "sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032"},
{file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"},
{file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"},
]
[package.dependencies]
click = ">=7.0"
h11 = ">=0.8"
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
[package.extras]
standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
@ -2749,16 +2665,19 @@ files = [
]
[[package]]
name = "zipstream-new"
version = "1.1.8"
description = "Zipfile generator that takes input files as well as streams"
name = "zipstream-ng"
version = "1.6.0"
description = "A modern and easy to use streamable zip file generator"
optional = false
python-versions = "*"
python-versions = ">=3.5.0"
files = [
{file = "zipstream-new-1.1.8.tar.gz", hash = "sha256:b031fe181b94e51678389d26b174bc76382605a078d7d5d8f5beae083f111c76"},
{file = "zipstream_new-1.1.8-py3-none-any.whl", hash = "sha256:0662eb3ebe764fa168a5883cd8819ef83b94bd9e39955537188459d2264a7f60"},
{file = "zipstream-ng-1.6.0.tar.gz", hash = "sha256:149dc502c0fcfb62718e89cb7e46380bd1c3409bb8479ed64ae779388b5321ac"},
{file = "zipstream_ng-1.6.0-py3-none-any.whl", hash = "sha256:e05a760a2f4d527c3fcfc73616a06fbd84dafc208218af19ccbdf3fca42de417"},
]
[package.extras]
tests = ["pytest", "pytest-cov"]
[[package]]
name = "zopfli"
version = "0.2.2"
@ -2836,4 +2755,4 @@ test = ["pytest"]
[metadata]
lock-version = "2.0"
python-versions = "~3.10"
content-hash = "a68893c2ce2e6a90bc58c20ef3250c5d1738c76b609b67896e1ae000c08d5f5e"
content-hash = "cae7a4ee44bbf095bd9257ea4a65908ef8ed6a7a3f68b1bbe7278567969bc16c"

View File

@ -23,11 +23,11 @@ drf-spectacular = { extras = ["sidecar"], version = "^0.26.3" }
psycopg = { extras = ["binary"], version = "^3.1.8" }
gunicorn = "^20.1.0"
uvicorn = "^0.21.1"
uvicorn = "^0.23.1"
whitenoise = "^6.4.0"
brotli = "^1.0.9"
requests = "^2.28.2"
httpx = "^0.23.3"
httpx = "^0.24.1"
jsonschema = "^4.17.3"
python-decouple = "^3.8"
@ -39,9 +39,8 @@ authlib = "^1.2.0"
python-gnupg = "^0.5.0"
lorem-text = "^2.1"
zipstream-new = "^1.1.8"
zipstream-ng = "^1.6.0"
boto3 = "^1.26.5"
pillow-heif = "^0.10.1"
playwright = "^1.32.1"
pikepdf = "^7.1.2"
celery = { extras = ["librabbitmq"], version = "^5.3" }

View File

@ -44,12 +44,12 @@ authlib==1.2.1 ; python_version >= "3.10" and python_version < "3.11" \
billiard==4.1.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:0f50d6be051c6b2b75bfbc8bfd85af195c5739c281d3f5b86a5640c65563614a \
--hash=sha256:1ad2eeae8e28053d729ba3373d34d9d6e210f6e4d8bf0a9c64f92bd053f1edf5
boto3==1.28.17 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:90f7cfb5e1821af95b1fc084bc50e6c47fa3edc99f32de1a2591faa0c546bea7 \
--hash=sha256:bca0526f819e0f19c0f1e6eba3e2d1d6b6a92a45129f98c0d716e5aab6d9444b
botocore==1.31.17 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:396459065dba4339eb4da4ec8b4e6599728eb89b7caaceea199e26f7d824a41c \
--hash=sha256:6ac34a1d34aa3750e78b77b8596617e2bab938964694d651939dba2cbde2c12b
boto3==1.28.23 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:807d4a4698ba9a76d5901a1663ff1943d13efbc388908f38b60f209c3511f1d6 \
--hash=sha256:839deb868d1278dd5a3f87208cfc4a8e259c95ca3cbe607cc322d435f02f63b0
botocore==1.31.23 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:d0a95f74eb6bd99e8f52f16af0a430ba6cd1526744f40ffdd3fcccceeaf961c2 \
--hash=sha256:f3258feaebce48f138eb2675168c4d33cc3d99e9f45af13cb8de47bdc2b9c573
brotli==1.0.9 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:02177603aaca36e1fd21b091cb742bb3b305a569e2402f1ca38af471777fb019 \
--hash=sha256:11d3283d89af7033236fa4e73ec2cbe743d4f6a81d41bd234f24bf63dde979df \
@ -414,25 +414,25 @@ cryptography==41.0.3 ; python_version >= "3.10" and python_version < "3.11" \
cssselect2==0.7.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:1ccd984dab89fc68955043aca4e1b03e0cf29cad9880f6e28e3ba7a74b14aa5a \
--hash=sha256:fd23a65bfd444595913f02fc71f6b286c29261e354c41d722ca7a261a49b5969
debugpy==1.6.7 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:0679b7e1e3523bd7d7869447ec67b59728675aadfc038550a63a362b63029d2c \
--hash=sha256:279d64c408c60431c8ee832dfd9ace7c396984fd7341fa3116aee414e7dcd88d \
--hash=sha256:33edb4afa85c098c24cc361d72ba7c21bb92f501104514d4ffec1fb36e09c01a \
--hash=sha256:38ed626353e7c63f4b11efad659be04c23de2b0d15efff77b60e4740ea685d07 \
--hash=sha256:5224eabbbeddcf1943d4e2821876f3e5d7d383f27390b82da5d9558fd4eb30a9 \
--hash=sha256:53f7a456bc50706a0eaabecf2d3ce44c4d5010e46dfc65b6b81a518b42866267 \
--hash=sha256:9cd10cf338e0907fdcf9eac9087faa30f150ef5445af5a545d307055141dd7a4 \
--hash=sha256:aaf6da50377ff4056c8ed470da24632b42e4087bc826845daad7af211e00faad \
--hash=sha256:b3e7ac809b991006ad7f857f016fa92014445085711ef111fdc3f74f66144096 \
--hash=sha256:bae1123dff5bfe548ba1683eb972329ba6d646c3a80e6b4c06cd1b1dd0205e9b \
--hash=sha256:c0ff93ae90a03b06d85b2c529eca51ab15457868a377c4cc40a23ab0e4e552a3 \
--hash=sha256:c4c2f0810fa25323abfdfa36cbbbb24e5c3b1a42cb762782de64439c575d67f2 \
--hash=sha256:d71b31117779d9a90b745720c0eab54ae1da76d5b38c8026c654f4a066b0130a \
--hash=sha256:dbe04e7568aa69361a5b4c47b4493d5680bfa3a911d1e105fbea1b1f23f3eb45 \
--hash=sha256:de86029696e1b3b4d0d49076b9eba606c226e33ae312a57a46dca14ff370894d \
--hash=sha256:e3876611d114a18aafef6383695dfc3f1217c98a9168c1aaf1a02b01ec7d8d1e \
--hash=sha256:ed6d5413474e209ba50b1a75b2d9eecf64d41e6e4501977991cdc755dc83ab0f \
--hash=sha256:f90a2d4ad9a035cee7331c06a4cf2245e38bd7c89554fe3b616d90ab8aab89cc
debugpy==1.6.7.post1 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:038c51268367c9c935905a90b1c2d2dbfe304037c27ba9d19fe7409f8cdc710c \
--hash=sha256:1093a5c541af079c13ac8c70ab8b24d1d35c8cacb676306cf11e57f699c02926 \
--hash=sha256:3370ef1b9951d15799ef7af41f8174194f3482ee689988379763ef61a5456426 \
--hash=sha256:38651c3639a4e8bbf0ca7e52d799f6abd07d622a193c406be375da4d510d968d \
--hash=sha256:3de5d0f97c425dc49bce4293df6a04494309eedadd2b52c22e58d95107e178d9 \
--hash=sha256:4b9eba71c290852f959d2cf8a03af28afd3ca639ad374d393d53d367f7f685b2 \
--hash=sha256:65b28435a17cba4c09e739621173ff90c515f7b9e8ea469b92e3c28ef8e5cdfb \
--hash=sha256:72f5d2ecead8125cf669e62784ef1e6300f4067b0f14d9f95ee00ae06fc7c4f7 \
--hash=sha256:85969d864c45f70c3996067cfa76a319bae749b04171f2cdeceebe4add316155 \
--hash=sha256:890f7ab9a683886a0f185786ffbda3b46495c4b929dab083b8c79d6825832a52 \
--hash=sha256:903bd61d5eb433b6c25b48eae5e23821d4c1a19e25c9610205f5aeaccae64e32 \
--hash=sha256:92b6dae8bfbd497c90596bbb69089acf7954164aea3228a99d7e43e5267f5b36 \
--hash=sha256:973a97ed3b434eab0f792719a484566c35328196540676685c975651266fccf9 \
--hash=sha256:d16882030860081e7dd5aa619f30dec3c2f9a421e69861125f83cc372c94e57d \
--hash=sha256:d4ac7a4dba28801d184b7fc0e024da2635ca87d8b0a825c6087bb5168e3c0d28 \
--hash=sha256:eea8d8cfb9965ac41b99a61f8e755a8f50e9a20330938ad8271530210f54e09c \
--hash=sha256:f0851403030f3975d6e2eaa4abf73232ab90b98f041e3c09ba33be2beda43fcf \
--hash=sha256:fe87ec0182ef624855d05e6ed7e0b7cb1359d2ffa2a925f8ec2d22e98b75d0ca
deprecation==2.1.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff \
--hash=sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a
@ -484,41 +484,41 @@ execnet==2.0.2 ; python_version >= "3.10" and python_version < "3.11" \
fido2==1.1.2 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:6110d913106f76199201b32d262b2857562cc46ba1d0b9c51fbce30dc936c573 \
--hash=sha256:a3b7d7d233dec3a4fa0d6178fc34d1cce17b820005a824f6ab96917a1e3be8d8
fonttools[woff]==4.41.1 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:1df1b6f4c7c4bc8201eb47f3b268adbf2539943aa43c400f84556557e3e109c0 \
--hash=sha256:2a22b2c425c698dcd5d6b0ff0b566e8e9663172118db6fd5f1941f9b8063da9b \
--hash=sha256:33191f062549e6bb1a4782c22a04ebd37009c09360e2d6686ac5083774d06d95 \
--hash=sha256:38cdecd8f1fd4bf4daae7fed1b3170dfc1b523388d6664b2204b351820aa78a7 \
--hash=sha256:3ae64303ba670f8959fdaaa30ba0c2dabe75364fdec1caeee596c45d51ca3425 \
--hash=sha256:3d1f9471134affc1e3b1b806db6e3e2ad3fa99439e332f1881a474c825101096 \
--hash=sha256:4e3334d51f0e37e2c6056e67141b2adabc92613a968797e2571ca8a03bd64773 \
--hash=sha256:4edc795533421e98f60acee7d28fc8d941ff5ac10f44668c9c3635ad72ae9045 \
--hash=sha256:547ab36a799dded58a46fa647266c24d0ed43a66028cd1cd4370b246ad426cac \
--hash=sha256:59eba8b2e749a1de85760da22333f3d17c42b66e03758855a12a2a542723c6e7 \
--hash=sha256:704bccd69b0abb6fab9f5e4d2b75896afa48b427caa2c7988792a2ffce35b441 \
--hash=sha256:73ef0bb5d60eb02ba4d3a7d23ada32184bd86007cb2de3657cfcb1175325fc83 \
--hash=sha256:7763316111df7b5165529f4183a334aa24c13cdb5375ffa1dc8ce309c8bf4e5c \
--hash=sha256:849ec722bbf7d3501a0e879e57dec1fc54919d31bff3f690af30bb87970f9784 \
--hash=sha256:891cfc5a83b0307688f78b9bb446f03a7a1ad981690ac8362f50518bc6153975 \
--hash=sha256:952cb405f78734cf6466252fec42e206450d1a6715746013f64df9cbd4f896fa \
--hash=sha256:a7bbb290d13c6dd718ec2c3db46fe6c5f6811e7ea1e07f145fd8468176398224 \
--hash=sha256:a9b3cc10dc9e0834b6665fd63ae0c6964c6bc3d7166e9bc84772e0edd09f9fa2 \
--hash=sha256:aaaef294d8e411f0ecb778a0aefd11bb5884c9b8333cc1011bdaf3b58ca4bd75 \
--hash=sha256:afce2aeb80be72b4da7dd114f10f04873ff512793d13ce0b19d12b2a4c44c0f0 \
--hash=sha256:b0938ebbeccf7c80bb9a15e31645cf831572c3a33d5cc69abe436e7000c61b14 \
--hash=sha256:b2d1ee95be42b80d1f002d1ee0a51d7a435ea90d36f1a5ae331be9962ee5a3f1 \
--hash=sha256:b927e5f466d99c03e6e20961946314b81d6e3490d95865ef88061144d9f62e38 \
--hash=sha256:bdd729744ae7ecd7f7311ad25d99da4999003dcfe43b436cf3c333d4e68de73d \
--hash=sha256:c2071267deaa6d93cb16288613419679c77220543551cbe61da02c93d92df72f \
--hash=sha256:cac73bbef7734e78c60949da11c4903ee5837168e58772371bd42a75872f4f82 \
--hash=sha256:da2c2964bdc827ba6b8a91dc6de792620be4da3922c4cf0599f36a488c07e2b2 \
--hash=sha256:e16a9449f21a93909c5be2f5ed5246420f2316e94195dbfccb5238aaa38f9751 \
--hash=sha256:e5c2b0a95a221838991e2f0e455dec1ca3a8cc9cd54febd68cc64d40fdb83669 \
--hash=sha256:ec453a45778524f925a8f20fd26a3326f398bfc55d534e37bab470c5e415caa1 \
--hash=sha256:edee0900cf0eedb29d17c7876102d6e5a91ee333882b1f5abc83e85b934cadb5 \
--hash=sha256:f14f3ccea4cc7dd1b277385adf3c3bf18f9860f87eab9c2fb650b0af16800f55 \
--hash=sha256:f240d9adf0583ac8fc1646afe7f4ac039022b6f8fa4f1575a2cfa53675360b69 \
--hash=sha256:f48602c0b3fd79cd83a34c40af565fe6db7ac9085c8823b552e6e751e3a5b8be
fonttools[woff]==4.42.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:01cfe02416b6d416c5c8d15e30315cbcd3e97d1b50d3b34b0ce59f742ef55258 \
--hash=sha256:0a1466713e54bdbf5521f2f73eebfe727a528905ff5ec63cda40961b4b1eea95 \
--hash=sha256:0df8ef75ba5791e873c9eac2262196497525e3f07699a2576d3ab9ddf41cb619 \
--hash=sha256:10dac980f2b975ef74532e2a94bb00e97a95b4595fb7f98db493c474d5f54d0e \
--hash=sha256:150122ed93127a26bc3670ebab7e2add1e0983d30927733aec327ebf4255b072 \
--hash=sha256:1f81ed9065b4bd3f4f3ce8e4873cd6a6b3f4e92b1eddefde35d332c6f414acc3 \
--hash=sha256:27ec3246a088555629f9f0902f7412220c67340553ca91eb540cf247aacb1983 \
--hash=sha256:2d6dc3fa91414ff4daa195c05f946e6a575bd214821e26d17ca50f74b35b0fe4 \
--hash=sha256:329341ba3d86a36e482610db56b30705384cb23bd595eac8cbb045f627778e9d \
--hash=sha256:3fb2a69870bfe143ec20b039a1c8009e149dd7780dd89554cc8a11f79e5de86b \
--hash=sha256:4655c480a1a4d706152ff54f20e20cf7609084016f1df3851cce67cef768f40a \
--hash=sha256:48e82d776d2e93f88ca56567509d102266e7ab2fb707a0326f032fe657335238 \
--hash=sha256:57b68eab183fafac7cd7d464a7bfa0fcd4edf6c67837d14fb09c1c20516cf20b \
--hash=sha256:58c1165f9b2662645de9b19a8c8bdd636b36294ccc07e1b0163856b74f10bafc \
--hash=sha256:614b1283dca88effd20ee48160518e6de275ce9b5456a3134d5f235523fc5065 \
--hash=sha256:685a4dd6cf31593b50d6d441feb7781a4a7ef61e19551463e14ed7c527b86f9f \
--hash=sha256:6bd7e4777bff1dcb7c4eff4786998422770f3bfbef8be401c5332895517ba3fa \
--hash=sha256:703101eb0490fae32baf385385d47787b73d9ea55253df43b487c89ec767e0d7 \
--hash=sha256:83b98be5d291e08501bd4fc0c4e0f8e6e05b99f3924068b17c5c9972af6fff84 \
--hash=sha256:8ece1886d12bb36c48c00b2031518877f41abae317e3a55620d38e307d799b7e \
--hash=sha256:9c456d1f23deff64ffc8b5b098718e149279abdea4d8692dba69172fb6a0d597 \
--hash=sha256:9cd2363ea7728496827658682d049ffb2e98525e2247ca64554864a8cc945568 \
--hash=sha256:a9b55d2a3b360e0c7fc5bd8badf1503ca1c11dd3a1cd20f2c26787ffa145a9c7 \
--hash=sha256:ae7df0ae9ee2f3f7676b0ff6f4ebe48ad0acaeeeaa0b6839d15dbf0709f2c5ef \
--hash=sha256:ae881e484702efdb6cf756462622de81d4414c454edfd950b137e9a7352b3cb9 \
--hash=sha256:b8600ae7dce6ec3ddfb201abb98c9d53abbf8064d7ac0c8a0d8925e722ccf2a0 \
--hash=sha256:c36c904ce0322df01e590ba814d5d69e084e985d7e4c2869378671d79662a7d4 \
--hash=sha256:c8bf88f9e3ce347c716921804ef3a8330cb128284eb6c0b6c4b3574f3c580023 \
--hash=sha256:d40673b2e927f7cd0819c6f04489dfbeb337b4a7b10fc633c89bf4f34ecb9620 \
--hash=sha256:d54e600a2bcfa5cdaa860237765c01804a03b08404d6affcd92942fa7315ffba \
--hash=sha256:dfe7fa7e607f7e8b58d0c32501a3a7cac148538300626d1b930082c90ae7f6bd \
--hash=sha256:e35bed436726194c5e6e094fdfb423fb7afaa0211199f9d245e59e11118c576c \
--hash=sha256:f0290ea7f9945174bd4dfd66e96149037441eb2008f3649094f056201d99e293 \
--hash=sha256:fae4e801b774cc62cecf4a57b1eae4097903fced00c608d9e2bc8f84cd87b54a
greenlet==2.0.2 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:03a8f4f3430c3b3ff8d10a2a86028c660355ab637cee9333d63d66b56f09d52a \
--hash=sha256:0bf60faf0bc2468089bdc5edd10555bab6e85152191df713e2ab1fcc86382b5a \
@ -589,12 +589,12 @@ h11==0.14.0 ; python_version >= "3.10" and python_version < "3.11" \
html5lib==1.1 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:0d78f8fde1c230e99fe37986a60526d7049ed4bf8a9fadbad5f00e22e58e041d \
--hash=sha256:b2e5b40261e20f354d198eae92afc10d750afb487ed5e50f9c4eaf07c184146f
httpcore==0.16.3 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:c5d6f04e2fc530f39e0c077e6a30caa53f1451096120f1f38b954afd0b17c0cb \
--hash=sha256:da1fb708784a938aa084bde4feb8317056c55037247c787bd7e19eb2c2949dc0
httpx==0.23.3 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:9818458eb565bb54898ccb9b8b251a28785dd4a55afbc23d0eb410754fe7d0f9 \
--hash=sha256:a211fcce9b1254ea24f0cd6af9869b3d29aba40154e947d2a07bb499b3e310d6
httpcore==0.17.3 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888 \
--hash=sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87
httpx==0.24.1 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd \
--hash=sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd
idna==3.4 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
--hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
@ -610,9 +610,9 @@ jmespath==1.0.1 ; python_version >= "3.10" and python_version < "3.11" \
jsonschema-specifications==2023.7.1 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1 \
--hash=sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb
jsonschema==4.18.4 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:971be834317c22daaa9132340a51c01b50910724082c2c1a2ac87eeec153a3fe \
--hash=sha256:fb3642735399fa958c0d2aad7057901554596c63349f4f6b283c493cf692a25d
jsonschema==4.19.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:043dc26a3845ff09d20e4420d6012a9c91c9aa8999fa184e7efcfeccb41e32cb \
--hash=sha256:6e1e7569ac13be8139b2dd2c21a55d350066ee3f80df06c608b398cdc6f30e8f
kombu==5.3.1 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:48ee589e8833126fd01ceaa08f8a2041334e9f5894e5763c8486a550454551e9 \
--hash=sha256:fbd7572d92c0bf71c112a6b45163153dea5a7b6a701ec16b568c27d0fd2370f2
@ -722,9 +722,9 @@ lxml==4.9.3 ; python_version >= "3.10" and python_version < "3.11" \
packaging==23.1 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \
--hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f
phonenumberslite==8.13.17 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:5741de4b77a963f33585eb0e8ffa2632ea9987d6e50a38ac67f441e49422de69 \
--hash=sha256:bae91ba7822ed73adeac739b9f9f2ded295375542014f3374e593ad92eef49c4
phonenumberslite==8.13.18 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:40cef03b24f2bc5711fed2b53b72770ff58f6b7dbfff749822c91078d6e82481 \
--hash=sha256:a321f0decf3e4e080f005fda3fba5a791d9d14a3ca217974345ff452923c31e2
pikepdf==7.2.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:0e1607fda03a53a29a4a8e3fbacbde788804c78167ff251e1c1006f89539f306 \
--hash=sha256:1cc8d0be5a62ed9011bb519abc34907b5965b392995043719effc4b6a00e2052 \
@ -759,57 +759,6 @@ pikepdf==7.2.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:f3c97acce9b66a41b2759dc30ef57de8f38c7239c9b0e7a5febc196b764a2567 \
--hash=sha256:f458c4161e76a882a15ade4125a2f92faa7e5ce120d2e6530dd995aa3308971c \
--hash=sha256:f7451f176eb9828d8dd7cb3d4e00d4e0aa7f7d7d00331fe640bc20cf3328deb5
pillow-heif==0.10.1 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:05406e07d6640e122729e249ad6a2bf28c1aabe0dde0a71217ad54c36854e0e9 \
--hash=sha256:091e43a45b1ed155c65a3a99252ba5d1ea7ba9ba7e9880afa06997533abe4875 \
--hash=sha256:0ed8652a520a46aa936b816bb3fcd445aba5ae6678f444927dcd6e7f831e02db \
--hash=sha256:158dc0eabaadb13240d2bc14ce11047a661a4748e56423a5346c4ffa9831e0e3 \
--hash=sha256:16db680b312ea684b3b88a3f97b3b122df48e12a057351c3ed1f435dd0a634d2 \
--hash=sha256:2229077a834182477cfb8f665c4c42ce9766d90d746d74c7ab6d48945c8a6992 \
--hash=sha256:27c1b4e388fde47f690a0b8e4299a8da57329a35e1924444028865e0efd20430 \
--hash=sha256:28a3872f66d55d74ea4c18f1460ccba1bae20874100331b58dae6bbc240c63a5 \
--hash=sha256:2c791917a9e286f3d692f5c162dedf07e65ebab18c4df7ad7a5a109d395aaca9 \
--hash=sha256:2e34110c906035f9902bb7dee964384e33b45c4545cee0fc4f78bd06b6cffbe0 \
--hash=sha256:37dd748836c8d5d82ef5395cd8aee523dba5bc0c6a77353baacf7868de41eec3 \
--hash=sha256:3dece6099058422ab7a66b713e9fc3ea4e21946a95442c276956825602a0782c \
--hash=sha256:400b25a1110ef5dfe394255646bae5318779d2ec4c787792bd5ba72956df628f \
--hash=sha256:41610fae8e2494f605b7b5c2508f6c2688227a7cd3f2c71e1fff966fd9476297 \
--hash=sha256:41a75fbf044db03d3e5d64c8288b7ea3ba4b9575ff1078f1df814936f15d11b7 \
--hash=sha256:4bf6abce62e934e33dbd5cf8528c76c746397116a87128b913278554eb840c3b \
--hash=sha256:4d4b04bf35280f7d895ba783c4b7f7e3d0f139c99fd736e1831d2cfe06a41c10 \
--hash=sha256:50cbb535e9b776bd327d7344e22bec1f7457ae587487189a136339cf90952a99 \
--hash=sha256:5909585d1878dfe214a7bc6ae502ce6e1ee99cab88dd0669714c2d524f8509da \
--hash=sha256:5ade9dbfbc5653fcf345fd8db75fb4fec603b521b1a832f091a809258d2232b5 \
--hash=sha256:7b84073e2997f34062751e8dd0a644e3e8f6fd952265edfe7ee021531a939018 \
--hash=sha256:8173d2843207a1c3265e382e7dcb02d8d5f882b5cd8ab9a1701c5bf47639ae22 \
--hash=sha256:82143407c590122e1d36bf674d7d589d20ed76fac243a65d1704e6b0fbc14dde \
--hash=sha256:856a4f46a689bc037c0e51b8ceae1e7944907a2c8a3767dd4d72c9f781ed82b7 \
--hash=sha256:95c0e83ef5237b18ae5e4adc5e5c9261b23c13704abedf1bbb46cc44d086312a \
--hash=sha256:9c6880056df5898cada6f65b5dc6ba8259da1b570491c18da867420f32314512 \
--hash=sha256:9d67655cde69eb76f7b5a3f3b3069998d43c9cd157a1e41997fe165a44614401 \
--hash=sha256:a2722a220d898cbcd1e3d6bcb669a28cfcb240d05f41bcd57d4b78af991b32cc \
--hash=sha256:a49c5671f74d8d58e4a0d507a3cdbd37c28693f5ad50b5bed5983a2b693e572a \
--hash=sha256:af9bd9d8fc189451edb193f321214207bf890d0ac80ac697056def39fec7565d \
--hash=sha256:b1e50cab15f2531ea5bdda9b15e5f2d05bf023b607e4322bc600dd18e3783757 \
--hash=sha256:b21d19372d9a1cc22a6e639cc929bc3abae7f701ee7c8b66bad5302f36977eef \
--hash=sha256:bae92c3e9b348e367122b140fd7a744bdb087c551ac00efc2b486a410569d00f \
--hash=sha256:bc12fc70de7f59a313678255b9abc7acd4915032cdbdb887a402f1e6c632e95d \
--hash=sha256:c57bbb1a1aabb88efa72ba24300a3df733826ed8892d5bbcc8317b4262e95a03 \
--hash=sha256:c57dc8496e59d4d9b8f79e66be148e5c898704b7bbd65531d69352bce2e820f0 \
--hash=sha256:da5c734c9510ccb05f42199bedb6b0f126f9e8447e3bde3ad03f3882817ad08c \
--hash=sha256:dae1ca05c818abc31bbc259a17554c3dd9faca4d79618f06f0cc2439320c4f58 \
--hash=sha256:db7363f190faeda67b15cf774fddf6c658a5681abb8b9860dcbc47cc85d668f8 \
--hash=sha256:dc143d3f61b7a7d28f4200be9cdcf0149b5da44511d8faacb4778a9dc264e900 \
--hash=sha256:dd3b2bfa20f3af072c1a1fedbdee441b71972969e09efc6b0f9789b540d51899 \
--hash=sha256:dd6f4f01006dfa5cfefd1e960763e2f3bd829e0c6e6d8202462fc3f7d0b91dfd \
--hash=sha256:de3a2929e509a93981866fb9ec2f313ee349312009ca50ed1ca999c4039c31e1 \
--hash=sha256:e9745aab7ed2bb0e53548e1e2c906721b0bc76adedeb17e661ec9ccbd8b698fd \
--hash=sha256:ea6cf2255179bb667b75b834845083f23959fc3873c444a15f54cad415e501dd \
--hash=sha256:ef1c87acea720edf784fa3da77d3292f288de1c9f40e9808f4c6837dd167afc3 \
--hash=sha256:f62617d91e6656535fde6ddb61f413c27e81f2d58eb38201b62982a05a729acd \
--hash=sha256:f98a5c77626bfb1dfdc83939fe44eb11ab721edfd4ca516e8e9b8e3c0dcfbe13 \
--hash=sha256:fd01437bca86e61b252a0e730c2181b3dd3bfb57367c0473a8dca6db53be5818 \
--hash=sha256:ffa99da11b0328dc483976d5c4e62cccc75903e0bcc861e3d9fbce2752f0dff5
pillow==10.0.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:00e65f5e822decd501e374b0650146063fbb30a7264b4d2744bdd7b913e0cab5 \
--hash=sha256:040586f7d37b34547153fa383f7f9aed68b738992380ac911447bb78f2abe530 \
@ -879,64 +828,64 @@ pluggy==1.2.0 ; python_version >= "3.10" and python_version < "3.11" \
prompt-toolkit==3.0.39 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac \
--hash=sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88
psycopg-binary==3.1.9 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:03b08545ce1c627f4d5e6384eda2946660c4ba6ceb0a09ae47de07419f725669 \
--hash=sha256:055537a9c20efe9bf17cb72bd879602eda71de6f737ebafa1953e017c6a37fbe \
--hash=sha256:058ab0d79be0b229338f0e61fec6f475077518cba63c22c593645a69f01c3e23 \
--hash=sha256:07414daa86662f7657e9fabe49af85a32a975e92e6568337887d9c9ffedc224f \
--hash=sha256:09167f106e7685591b4cdf58eff0191fb7435d586f384133a0dd30df646cf409 \
--hash=sha256:17c5d4936c746f5125c6ef9eb43655e27d4d0c9ffe34c3073878b43c3192511d \
--hash=sha256:1e31bac3d2d41e6446b20b591f638943328c958f4d1ce13d6f1c5db97c3a8dee \
--hash=sha256:2340ca2531f69e5ebd9d18987362ba57ed6ab6a271511d8026814a46a2a87b59 \
--hash=sha256:25e3ce947aaaa1bd9f1920fca76d7281660646304f9ea5bc036b201dd8790655 \
--hash=sha256:284038cbe3f5a0f3de417af9b5eaa2a9524a3a06211523cf245111c71b566506 \
--hash=sha256:2f94a7985135e084e122b143956c6f589d17aef743ecd0a434a3d3a222631d5a \
--hash=sha256:3836bdaf030a5648bd5f5b452e4b068b265e28f9199060c5b70dbf4a218cde6e \
--hash=sha256:3a82e77400d1ef6c5bbcf3e600e8bdfacf1a554512f96c090c43ceca3d1ce3b6 \
--hash=sha256:3b62545cc64dd69ea0ae5ffe18d7c97e03660ab8244aa8c5172668a21c41daa0 \
--hash=sha256:3b816ce0e27a2a8786d34b61d3e36e01029245025879d64b88554326b794a4f0 \
--hash=sha256:3bb86d58b90faefdc0bbedf08fdea4cc2afcb1cfa4340f027d458bfd01d8b812 \
--hash=sha256:3d91ee0d33ac7b42d0488a9be2516efa2ec00901b81d69566ff34a7a94b66c0b \
--hash=sha256:4213953da44324850c8f789301cf665f46fb94301ba403301e7af58546c3a428 \
--hash=sha256:4c1def6c2d28e257325b3b208cf1966343b498282a0f4d390fda7b7e0577da64 \
--hash=sha256:53afb0cc2ebe74651f339e22d05ec082a0f44939715d9138d357852f074fcf55 \
--hash=sha256:5833bda4c14f24c6a8ac08d3c5712acaa4f35aab31f9ccd2265e9e9a7d0151c8 \
--hash=sha256:5b164355d023a91b23dcc4bb3112bc7d6e9b9c938fb5abcb6e54457d2da1f317 \
--hash=sha256:5cdc13c8ec1437240801e43d07e27ff6479ac9dd8583ecf647345bfd2e8390e4 \
--hash=sha256:63e8d1dbe253657c70dbfa9c59423f4654d82698fc5ed6868b8dc0765abe20b6 \
--hash=sha256:6c696dc84f9ff155761df15779181d8e4af7746b98908e130add8259912e4bb7 \
--hash=sha256:7b36fe4314a784fbe45c9fd71c902b9bf57341aff9b97c0cbd22f8409a271e2f \
--hash=sha256:81e34d6df54329424944d5ca91b1cc77df6b8a9130cb5480680d56f53d4e485c \
--hash=sha256:82704a899d57c29beba5399d41eab5ef5c238b810d7e25e2d1916d2b34c4b1a3 \
--hash=sha256:87e0c97733b11eeca3d24e56df70f3f9d792b2abd46f48be2fb2348ffc3e7e39 \
--hash=sha256:90787ac05b932c0fc678cbf470ccea9c385b8077583f0490136b4569ed3fb652 \
--hash=sha256:96725d9691a84a21eb3e81c884a2e043054e33e176801a57a05e9ac38d142c6e \
--hash=sha256:98d9d156b9ada08c271a79662fc5fcc1731b4d7c1f651ef5843d818d35f15ba0 \
--hash=sha256:9c114f678e8f4a96530fa79cfd84f65f26358ecfc6cca70cfa2d5e3ae5ef217a \
--hash=sha256:9c75be2a9b986139e3ff6bc0a2852081ac00811040f9b82d3aa539821311122e \
--hash=sha256:a207d5a7f4212443b7452851c9ccd88df9c6d4d58fa2cea2ead4dd9cb328e578 \
--hash=sha256:a274c63c8fb9d419509bed2ef72befc1fd04243972e17e7f5afc5725cb13a560 \
--hash=sha256:a8aaa47c1791fc05c0229ec1003dd49e13238fba9434e1fc3b879632f749c3c4 \
--hash=sha256:b1a321ef3579a8de0545ade6ff1edfde0c88b8847d58c5615c03751c76054796 \
--hash=sha256:b246fed629482b06f938b23e9281c4af592329daa3ec2cd4a6841ccbfdeb4d68 \
--hash=sha256:c0b8d6bbeff1dba760a208d8bc205a05b745e6cee02b839f969f72cf56a8b80d \
--hash=sha256:c3a13aa022853891cadbc7256a9804e5989def760115c82334bddf0d19783b0b \
--hash=sha256:c7d990f14a37345ca05a5192cd5ac938c9cbedca9c929872af6ae311158feb0e \
--hash=sha256:ce8f4dea5934aa6c4933e559c74bef4beb3413f51fbcf17f306ce890216ac33a \
--hash=sha256:d2cea4bb0b19245c83486868d7c66f73238c4caa266b5b3c3d664d10dab2ab56 \
--hash=sha256:dade344aa90bb0b57d1cfc13304ed83ab9a36614b8ddd671381b2de72fe1483d \
--hash=sha256:db866cc557d9761036771d666d17fa4176c537af7e6098f42a6bf8f64217935f \
--hash=sha256:dfe5c5c31f59ccb1d1f473466baa93d800138186286e80e251f930e49c80d208 \
--hash=sha256:e0ca74fd85718723bb9f08e0c6898e901a0c365aef20b3c3a4ef8709125d6210 \
--hash=sha256:e2f463079d99568a343ed0b766150b30627e9ed41de99fd82e945e7e2bec764a \
--hash=sha256:eab449e39db1c429cac79b7aa27e6827aad4995f32137e922db7254f43fed7b5 \
--hash=sha256:f2cbdef6568da21c39dfd45c2074e85eabbd00e1b721832ba94980f01f582dd4 \
--hash=sha256:f41a9e0de4db194c053bcc7c00c35422a4d19d92a8187e8065b1c560626efe35 \
--hash=sha256:f4da4ca9b2365fc1d3fc741c3bbd3efccd892ce813444b884c8911a1acf1c932 \
--hash=sha256:f5e36504373e5bcdc954b1da1c6fe66379007fe1e329790e8fb72b879a01e097
psycopg[binary]==3.1.9 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:ab400f207a8c120bafdd8077916d8f6c0106e809401378708485b016508c30c9 \
--hash=sha256:fbbac339274d8733ee70ba9822297af3e8871790a26e967b5ea53e30a4b74dcc
psycopg-binary==3.1.10 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:0471869e658d0c6b8c3ed53153794739c18d7dad2dd5b8e6ff023a364c20f7df \
--hash=sha256:0f062f20256708929a58c41d44f350efced4c00a603323d1413f6dc0b84d95a5 \
--hash=sha256:1583ced5948cf88124212c4503dfe5b01ac3e2dd1a2833c083917f4c4aabe8b4 \
--hash=sha256:1e46b97073bd4de114f475249d681eaf054e950699c5d7af554d3684db39b82d \
--hash=sha256:2098721c486478987be700723b28ec7a48f134eba339de36af0e745f37dfe461 \
--hash=sha256:30eb731ed5525d8df892db6532cc8ffd8a163b73bc355127dee9c49334e16eee \
--hash=sha256:32caf98cb00881bfcbbbae39a15f2a4e08b79ff983f1c0f13b60a888ef6e8431 \
--hash=sha256:36fff836a7823c9d71fa7faa333c74b2b081af216cebdbb0f481dce55ee2d974 \
--hash=sha256:3b6c6f90241c4c5a6ca3f0d8827e37ef90fdc4deb9d8cfa5678baa0ea374b391 \
--hash=sha256:415961e839bb49cfd75cd961503fb8846c0768f247db1fa7171c1ac61d38711b \
--hash=sha256:41a415e78c457b06497fa0084e4ea7245ca1a377b55756dd757034210b64da7e \
--hash=sha256:4290060ee0d856caa979ecf675c0e6959325f508272ccf27f64c3801c7bcbde7 \
--hash=sha256:4a3a7e99ba10c2e83a48d79431560e0d5ca7865f68f2bac3a462dc2b151e9926 \
--hash=sha256:50bf7a59d3a85a82d466fed341d352b44d09d6adc18656101d163a7cfc6509a0 \
--hash=sha256:511d38b1e1961d179d47d5103ba9634ecfc7ead431d19a9337ef82f3a2bca807 \
--hash=sha256:51fe70708243b83bf16710d8c11b61bd46562e6a24a6300d5434380b35911059 \
--hash=sha256:5565a6a86fee8d74f30de89e07f399567cdf59367aeb09624eb690d524339076 \
--hash=sha256:57b93c756fee5f7c7bd580c34cd5d244f7d5638f8b2cf25333f97b9b8b2ebfd1 \
--hash=sha256:666e7acf2ffdb5e8a58e8b0c1759facdb9688c7e90ee8ca7aed675803b57404d \
--hash=sha256:6670d160d054466e8fdedfbc749ef8bf7dfdf69296048954d24645dd4d3d3c01 \
--hash=sha256:6a691dc8e2436d9c1e5cf93902d63e9501688fccc957eb22f952d37886257470 \
--hash=sha256:747176a6aeb058079f56c5397bd90339581ab7b3cc0d62e7445654e6a484c7e1 \
--hash=sha256:74ce92122be34cf0e5f06d79869e1001c8421a68fa7ddf6fe38a717155cf3a64 \
--hash=sha256:75608a900984061c8898be68fbddc6f3da5eefdffce6e0624f5371645740d172 \
--hash=sha256:7e61f7b412fca7b15dd043a0b22fd528d2ed8276e76b3764c3889e29fa65082b \
--hash=sha256:848f4f4707dc73f4b4e844c92f3de795b2ddb728f75132602bda5e6ba55084fc \
--hash=sha256:88caa5859740507b3596c6c2e00ceaccee2c6ab5317bc535887801ad3cc7f3e1 \
--hash=sha256:8b658f7f8b49fb60a1c52e3f6692f690a85bdf1ad30aafe0f3f1fd74f6958cf8 \
--hash=sha256:908fa388a5b75dfd17a937acb24708bd272e21edefca9a495004c6f70ec2636a \
--hash=sha256:9cf56bb4b115def3a18157f3b3b7d8322ee94a8dea30028db602c8f9ae34ad1e \
--hash=sha256:9fb0d64520b29bd80a6731476ad8e1c20348dfdee00ab098899d23247b641675 \
--hash=sha256:a1d61b7724c7215a8ea4495a5c6b704656f4b7bb6165f4cb9989b685886ebc48 \
--hash=sha256:a4cbaf12361136afefc5faab21a174a437e71c803b083f410e5140c7605bc66b \
--hash=sha256:a4e91e1a8d61c60f592a1dfcebdf55e52a29fe4fdb650c5bd5414c848e77d029 \
--hash=sha256:a529c203f6e0f4c67ba27cf8f9739eb3bc880ad70d6ad6c0e56c2230a66b5a09 \
--hash=sha256:a7bbe9017edd898d7b3a8747700ed045dda96a907dff87f45e642e28d8584481 \
--hash=sha256:abf04bc06c8f6a1ac3dc2106d3b79c8661352e9d8a57ca2934ffa6aae8fe600a \
--hash=sha256:b30887e631fd67affaed98f6cd2135b44f2d1a6d9bca353a69c3889c78bd7aa8 \
--hash=sha256:b9d88ac72531034ebf7ec09114e732b066a9078f4ce213cf65cc5e42eb538d30 \
--hash=sha256:ba7812a593c16d9d661844dc8dd4d81548fd1c2a0ee676f3e3d8638369f4c5e4 \
--hash=sha256:bd6e14d1aeb12754a43446c77a5ce819b68875cc25ae6538089ef90d7f6dd6f7 \
--hash=sha256:bfc05ed4e74fa8615d7cc2bd57f00f97662f4e865a731dbd43da9a527e289c8c \
--hash=sha256:c5b59c8cff887757ddf438ff9489d79c5e6b717112c96f5c68e16f367ff8724e \
--hash=sha256:caa771569da01fc0389ca34920c331a284425a68f92d1ba0a80cc08935f8356e \
--hash=sha256:d32026cfab7ba7ac687a42c33345026a2fb6fc5608a6144077f767af4386be0b \
--hash=sha256:dea30f2704337ca2d0322fccfe1fa30f61ce9185de3937eb986321063114a51f \
--hash=sha256:e0f33e33a072e3d5af51ee4d4a439e10dbe623fe87ef295d5d688180d529f13f \
--hash=sha256:f2bea0940d69c3e24a72530730952687912893b34c53aa39e79045e7b446174d \
--hash=sha256:f48665947c55f8d6eb3f0be98de80411508e1ec329f354685329b57fced82c7f \
--hash=sha256:f6f7738c59262d8d19154164d99c881ed58ed377fb6f1d685eb0dc43bbcd8022 \
--hash=sha256:f7187269d825e84c945be7d93dd5088a4e0b6481a4bdaba3bf7069d4ac13703d \
--hash=sha256:fa92661f99351765673835a4d936d79bd24dfbb358b29b084d83be38229a90e4 \
--hash=sha256:ff72576061c774bcce5f5440b93e63d4c430032dd056d30f6cb1988e549dd92c \
--hash=sha256:ffc8c796194f23b9b07f6d25f927ec4df84a194bbc7a1f9e73316734eef512f9
psycopg[binary]==3.1.10 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:15b25741494344c24066dc2479b0f383dd1b82fa5e75612fa4fa5bb30726e9b6 \
--hash=sha256:8bbeddae5075c7890b2fa3e3553440376d3c5e28418335dee3c3656b06fa2b52
pycparser==2.21 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9 \
--hash=sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206
@ -1056,15 +1005,12 @@ pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "3.11" \
qrcode[pil]==7.4.2 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a \
--hash=sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845
referencing==0.30.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:47237742e990457f7512c7d27486394a9aadaf876cbfaa4be65b27b4f4d47c6b \
--hash=sha256:c257b08a399b6c2f5a3510a50d28ab5dbc7bbde049bcaf954d43c446f83ab548
referencing==0.30.2 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf \
--hash=sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0
requests==2.31.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
--hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
rfc3986[idna2008]==1.5.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835 \
--hash=sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97
rpds-py==0.9.2 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:0173c0444bec0a3d7d848eaeca2d8bd32a1b43f3d3fde6617aac3731fa4be05f \
--hash=sha256:01899794b654e616c8625b194ddd1e5b51ef5b60ed61baa7a2d9c2ad7b2a4238 \
@ -1196,9 +1142,9 @@ uritemplate==4.1.1 ; python_version >= "3.10" and python_version < "3.11" \
urllib3==1.26.16 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f \
--hash=sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14
uvicorn==0.21.1 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:0fac9cb342ba099e0d582966005f3fdba5b0290579fed4a6266dc702ca7bb032 \
--hash=sha256:e47cac98a6da10cd41e6fd036d472c6f58ede6c5dbee3dbee3ef7a100ed97742
uvicorn==0.23.2 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53 \
--hash=sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a
vine==5.0.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30 \
--hash=sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e
@ -1318,9 +1264,9 @@ wrapt==1.15.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09 \
--hash=sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559 \
--hash=sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639
zipstream-new==1.1.8 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:0662eb3ebe764fa168a5883cd8819ef83b94bd9e39955537188459d2264a7f60 \
--hash=sha256:b031fe181b94e51678389d26b174bc76382605a078d7d5d8f5beae083f111c76
zipstream-ng==1.6.0 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:149dc502c0fcfb62718e89cb7e46380bd1c3409bb8479ed64ae779388b5321ac \
--hash=sha256:e05a760a2f4d527c3fcfc73616a06fbd84dafc208218af19ccbdf3fca42de417
zopfli==0.2.2 ; python_version >= "3.10" and python_version < "3.11" \
--hash=sha256:00a66579f2e663cd7eabad71f5b114abf442f4816fdaf251b4b495aa9d016a67 \
--hash=sha256:01e82e6e31cfcb2eb7e3d6d72d0a498d150e3c3112cae3b5ab88ca3efedbc162 \

View File

View File

@ -91,15 +91,15 @@ def backup_files(z, path, storage, models):
else:
qs = qs.distinct()
for f in qs.iterator():
z.write_iter(str(Path(path) / f), file_chunks(f))
z.add(arcname=str(Path(path) / f), data=file_chunks(f))
def create_backup():
logging.info('Backup requested')
z = zipstream.ZipFile(mode='w', compression=zipstream.ZIP_DEFLATED, allowZip64=True)
z.writestr('VERSION', settings.VERSION.encode())
z.writestr('migrations.json', json.dumps(create_migration_info()).encode())
z.write_iter('backup.jsonl', create_database_dump())
z = zipstream.ZipStream(compress_type=zipstream.ZIP_DEFLATED)
z.add(arcname='VERSION', data=settings.VERSION.encode())
z.add(arcname='migrations.json', data=json.dumps(create_migration_info()).encode())
z.add(arcname='backup.jsonl', data=create_database_dump())
backup_files(z, 'uploadedimages', storages.get_uploaded_image_storage(), [UploadedImage, UploadedUserNotebookImage, UploadedTemplateImage])
backup_files(z, 'uploadedassets', storages.get_uploaded_asset_storage(), [UploadedAsset])

View File

@ -177,6 +177,9 @@ def import_archive(archive_file, serializer_classes: list[Type[serializers.Seria
f.delete(save=False)
except Exception:
log.exception(f'Failed to delete imported file "{f.name}" during rollback')
if isinstance(ex, tarfile.ReadError):
raise serializers.ValidationError(detail='Could not read .tar.gz file') from ex
raise ex

View File

@ -303,7 +303,8 @@ class ProjectTypeExportImportSerializer(ExportImportSerializer):
model = ProjectType
fields = [
'format', 'id', 'created', 'updated', 'name', 'language',
'report_fields', 'report_sections', 'finding_fields', 'finding_field_order',
'report_fields', 'report_sections',
'finding_fields', 'finding_field_order', 'finding_ordering',
'report_template', 'report_styles', 'report_preview_data',
'assets'
]
@ -335,7 +336,7 @@ class PentestFindingExportImportSerializer(ExportImportSerializer):
class Meta:
model = PentestFinding
fields = [
'id', 'created', 'updated', 'assignee', 'status', 'template', 'data',
'id', 'created', 'updated', 'assignee', 'status', 'template', 'order', 'data',
]
extra_kwargs = {'created': {'read_only': False}}
@ -343,18 +344,15 @@ class PentestFindingExportImportSerializer(ExportImportSerializer):
project = self.context['project']
data = validated_data.pop('data_all', {})
template = validated_data.pop('template_id', None)
finding = PentestFinding(**{
return PentestFinding.objects.create(**{
'project': project,
'template_id': template.id if template else None,
'data': ensure_defined_structure(
value=data,
definition=project.project_type.finding_fields_obj,
handle_undefined=HandleUndefinedFieldsOptions.FILL_NONE,
include_unknown=True)
} | validated_data)
finding.update_data(ensure_defined_structure(
value=data,
definition=project.project_type.finding_fields_obj,
handle_undefined=HandleUndefinedFieldsOptions.FILL_NONE,
include_unknown=True)
)
finding.save()
return finding
class ReportSectionExportImportSerializer(ExportImportSerializer):
@ -417,7 +415,7 @@ class PentestProjectExportImportSerializer(ExportImportSerializer):
model = PentestProject
fields = [
'format', 'id', 'created', 'updated', 'name', 'language', 'tags',
'members', 'pentesters', 'project_type',
'members', 'pentesters', 'project_type', 'override_finding_order',
'report_data', 'sections', 'findings', 'notes', 'images', 'files',
]
extra_kwargs = {

View File

@ -325,9 +325,6 @@ STORAGES = {
},
}
from pillow_heif import register_heif_opener
register_heif_opener()
# Default primary key field type
# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field

View File

@ -1,12 +1,12 @@
{
"$id": "https://syslifters.com/reportcreator/fielddefinition.schem.json",
"$id": "https://sysreptor.com/schema/fielddefinition.schem.json",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Field Definition",
"$defs": {
"field_object": {
"type": "object",
"patternProperties": {
"^[a-zA-Z_][a-zA-Z0-9_]+$": {
"^[a-zA-Z_][a-zA-Z0-9_]*$": {
"$ref": "#/$defs/field_value",
"required": ["type", "label"]
}

View File

@ -0,0 +1,21 @@
{
"$id": "https://sysreptor.com/schema/findingordering.schem.json",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Finding Ordering Definition",
"type": "array",
"items": {
"type": "object",
"required": ["field", "order"],
"properties": {
"field": {
"type": "string",
"format": "^[a-zA-Z0-9_-]+$",
"maxLength": 255
},
"order": {
"type": "string",
"enum": ["asc", "desc"]
}
}
}
}

View File

@ -6,10 +6,10 @@ from reportcreator_api.utils.utils import copy_keys
# These fields are required internally and cannot be removed or changed
FINDING_FIELDS_CORE = {
'title': StringField(origin=FieldOrigin.CORE, label='Title', spellcheck=True, default='TODO: Finding Title'),
'cvss': CvssField(origin=FieldOrigin.CORE, label='CVSS', default='n/a'),
}
# Prdefined fields are a set of fields which
FINDING_FIELDS_PREDEFINED = {
'cvss': CvssField(origin=FieldOrigin.PREDEFINED, label='CVSS', default='n/a'),
'summary': MarkdownField(origin=FieldOrigin.PREDEFINED, label='Summary', required=True, default='TODO: High-level summary'),
'description': MarkdownField(origin=FieldOrigin.PREDEFINED, label='Technical Description', required=True, default='TODO: detailed technical description what this findings is about and how it can be exploited'),
'precondition': StringField(origin=FieldOrigin.PREDEFINED, label='Precondition', required=True, spellcheck=True, default=None),
@ -46,6 +46,13 @@ FINDING_FIELDS_PREDEFINED = {
EnumChoice(value='CLNT', label='CLNT - Client-side Testing'),
EnumChoice(value='APIT', label='APIT - API Testing'),
]),
'severity': EnumField(origin=FieldOrigin.PREDEFINED, label='Severity', required=True, default=None, choices=[
EnumChoice(value='info', label='Info'),
EnumChoice(value='low', label='Low'),
EnumChoice(value='medium', label='Medium'),
EnumChoice(value='high', label='High'),
EnumChoice(value='critical', label='Critical'),
]),
'retest_notes': MarkdownField(origin=FieldOrigin.PREDEFINED, label='Re-test Notes', required=False, default=None),
'retest_status': EnumField(origin=FieldOrigin.PREDEFINED, label='Re-test Status', required=False, default=None, choices=[
@ -68,7 +75,7 @@ REPORT_FIELDS_PREDEFINED = {
def finding_fields_default():
return field_definition_to_dict(
FINDING_FIELDS_CORE | copy_keys(FINDING_FIELDS_PREDEFINED, ['summary', 'description', 'impact', 'recommendation', 'affected_components', 'references']) | {
FINDING_FIELDS_CORE | copy_keys(FINDING_FIELDS_PREDEFINED, ['cvss', 'summary', 'description', 'impact', 'recommendation', 'affected_components', 'references']) | {
'short_recommendation': StringField(label='Short Recommendation', required=True, default='TODO: short recommendation'),
})
@ -82,6 +89,13 @@ def finding_field_order_default():
]
def finding_ordering_default():
return [
{'field': 'cvss', 'order': 'desc'},
{'field': 'title', 'order': 'asc'},
]
def report_fields_default():
return field_definition_to_dict(REPORT_FIELDS_CORE | {

View File

@ -1,5 +1,5 @@
{
"$id": "https://syslifters.com/reportcreator/sectionddefinition.schem.json",
"$id": "https://sysreptor.com/schema/sectionddefinition.schem.json",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "Section Definition",
"type": "array",

View File

@ -0,0 +1,78 @@
import enum
import functools
import logging
from reportcreator_api.pentests import cvss
from reportcreator_api.pentests.customfields.types import FieldDataType
class SortOrder(enum.Enum):
ASC = 'asc'
DESC = 'desc'
@functools.total_ordering
class SortKeyPart:
def __init__(self, value, order: SortOrder) -> None:
self.value = value
self.order = order
def __eq__(self, other) -> bool:
return self.value == other.value
def __lt__(self, other) -> bool:
if self.order == SortOrder.DESC:
return other.value < self.value
else:
return self.value < other.value
def __repr__(self) -> str:
return f'SortKeyPart(value={self.value}, order={self.order})'
def format_sortable_field(value, definition):
if definition.type in [FieldDataType.OBJECT, FieldDataType.LIST, FieldDataType.USER]:
logging.warning('Sorting by unsupported data type. Ignoring field.')
return ''
elif definition.type == FieldDataType.CVSS:
return float(value['score'])
elif definition.type == FieldDataType.ENUM:
# Sort enums by position of choice in choices list, not by value
return next((i for i, c in enumerate(definition.choices) if c.value == value['value']), -1)
elif value is not None:
return value
elif definition.type == FieldDataType.BOOLEAN:
return False
elif definition.type == FieldDataType.NUMBER:
return 0
# STRING, MARKDOWN, COMBOBOX, DATE, USER
return ''
def sort_findings_by_fields(findings, project_type):
def get_sort_key(finding):
out = []
for order_field_config in project_type.finding_ordering:
out.append(SortKeyPart(
value=format_sortable_field(
value=finding.get(order_field_config['field']),
definition=project_type.finding_fields_obj[order_field_config['field']]),
order=SortOrder(order_field_config.get('order', 'asc'))))
# Always sort by created as last key to ensure consistent ordering
out.append(SortKeyPart(finding.get('created'), order=SortOrder.ASC))
return out
return sorted(findings, key=get_sort_key)
def sort_findings_by_order(findings):
return sorted(findings, key=lambda f: (f.get('order', 0), f.get('created', '')))
def sort_findings(findings, project_type, override_finding_order=False):
if override_finding_order:
return sort_findings_by_order(findings)
else:
return sort_findings_by_fields(findings, project_type)

View File

@ -1,6 +1,7 @@
import functools
import itertools
import json
from typing import Any
import jsonschema
from pathlib import Path
from django.core.exceptions import ValidationError
@ -21,6 +22,12 @@ def get_section_definition_schema():
return jsonschema.Draft202012Validator(schema=json.loads((Path(__file__).parent / 'sectiondefinition.schema.json').read_text()))
@functools.cache
def get_finding_ordering_schema():
return jsonschema.Draft202012Validator(schema=json.loads((Path(__file__).parent / 'findingordering.schema.json').read_text()))
@deconstructible
class FieldDefinitionValidator:
def __init__(self, core_fields=None, predefined_fields=None) -> None:
@ -49,7 +56,10 @@ class FieldDefinitionValidator:
except jsonschema.ValidationError as ex:
raise ValidationError('Invalid field definition') from ex
parsed_value = parse_field_definition(value)
try:
parsed_value = parse_field_definition(value)
except Exception as ex:
raise ValidationError('Invalid field definition') from ex
# validate core fields:
# required
# structure cannot be changed
@ -137,3 +147,11 @@ class SectionDefinitionValidator:
if len(section_fields) != len(set(section_fields)):
raise ValidationError('Invalid section definition: Field in multiple sections')
@deconstructible
class FindingOrderingValidator:
def __call__(self, value):
try:
get_finding_ordering_schema().validate(value)
except jsonschema.ValidationError as ex:
raise ValidationError('Invalid finding ordering') from ex

View File

@ -394,45 +394,44 @@ def is_cvss(vector):
return is_cvss3_1(vector) or is_cvss3_0(vector) or is_cvss2(vector)
def calculate_score(vector, return_metrics=False) -> Union[float, dict]:
def calculate_metrics(vector) -> dict:
if (metrics := calculate_score_cvss3_1(vector)) is not None:
return metrics
elif (metrics := calculate_score_cvss3_0(vector)) is not None:
return metrics
elif (metrics := calculate_score_cvss2(vector)) is not None:
return metrics
return {
"version": None,
"base": {
"score": 0.0,
"exploitability": 0.0,
"impact": 0.0
},
"temporal": {
"score": 0.0,
"exploitability": 0.0,
"impact": 0.0
},
"environmental": {
"score": 0.0,
"exploitability": 0.0,
"impact": 0.0
},
"final": {
"score": 0.0,
"exploitability": 0.0,
"impact": 0.0
},
}
def calculate_score(vector) -> float:
"""
Calculate the CVSS score from a CVSS vector.
Supports CVSS v2, v3.0 and v3.1
"""
if (score := calculate_score_cvss3_1(vector)) is not None:
pass
elif (score := calculate_score_cvss3_0(vector)) is not None:
pass
elif (score := calculate_score_cvss2(vector)) is not None:
pass
if score is None:
score = {
"version": None,
"base": {
"score": 0.0,
"exploitability": 0.0,
"impact": 0.0
},
"temporal": {
"score": 0.0,
"exploitability": 0.0,
"impact": 0.0
},
"environmental": {
"score": 0.0,
"exploitability": 0.0,
"impact": 0.0
},
"final": {
"score": 0.0,
"exploitability": 0.0,
"impact": 0.0
},
}
if return_metrics:
return score
else:
return score["final"]["score"]
return calculate_metrics(vector)['final']['score']
def level_from_score(score: float) -> CVSSLevel:

View File

@ -0,0 +1,31 @@
# Generated by Django 4.2.4 on 2023-08-11 07:53
import django.core.serializers.json
from django.db import migrations, models
import reportcreator_api.pentests.customfields.predefined_fields
import reportcreator_api.pentests.customfields.validators
class Migration(migrations.Migration):
dependencies = [
('pentests', '0040_uploadedtemplateimage'),
]
operations = [
migrations.AddField(
model_name='pentestfinding',
name='order',
field=models.PositiveIntegerField(db_index=True, default=0),
),
migrations.AddField(
model_name='pentestproject',
name='override_finding_order',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='projecttype',
name='finding_ordering',
field=models.JSONField(default=reportcreator_api.pentests.customfields.predefined_fields.finding_ordering_default, encoder=django.core.serializers.json.DjangoJSONEncoder, validators=[reportcreator_api.pentests.customfields.validators.FindingOrderingValidator()]),
),
]

View File

@ -8,17 +8,18 @@ from django.utils.translation import gettext_lazy as _
from reportcreator_api.archive.crypto.fields import EncryptedField
from reportcreator_api.pentests.customfields.mixins import EncryptedCustomFieldsMixin
from reportcreator_api.pentests.customfields.predefined_fields import FINDING_FIELDS_CORE, FINDING_FIELDS_PREDEFINED, REPORT_FIELDS_CORE, REPORT_FIELDS_PREDEFINED, finding_field_order_default, finding_fields_default, report_fields_default, report_sections_default
from reportcreator_api.pentests.customfields.types import FieldDefinition, field_definition_to_dict, parse_field_definition
from reportcreator_api.pentests.customfields.predefined_fields import FINDING_FIELDS_CORE, FINDING_FIELDS_PREDEFINED, \
REPORT_FIELDS_CORE, REPORT_FIELDS_PREDEFINED, finding_field_order_default, finding_fields_default, report_fields_default, \
finding_ordering_default, report_sections_default
from reportcreator_api.pentests.customfields.types import FieldDataType, FieldDefinition, field_definition_to_dict, parse_field_definition
from reportcreator_api.pentests.customfields.utils import HandleUndefinedFieldsOptions, ensure_defined_structure, set_field_origin
from reportcreator_api.pentests.customfields.validators import FieldDefinitionValidator, SectionDefinitionValidator
from reportcreator_api.pentests.customfields.validators import FieldDefinitionValidator, FindingOrderingValidator, SectionDefinitionValidator
from reportcreator_api.pentests.models.common import ImportableMixin, Language, LanguageMixin, LockableMixin, ReviewStatus
from reportcreator_api.users.models import PentestUser
from reportcreator_api.utils.decorators import cache
from reportcreator_api.utils.error_messages import ErrorMessage
from reportcreator_api.utils.models import BaseModel
from reportcreator_api.pentests import querysets
from reportcreator_api.pentests import cvss as cvss_utils
from reportcreator_api.utils.utils import remove_duplicates
@ -49,6 +50,7 @@ class ProjectType(LockableMixin, LanguageMixin, ImportableMixin, BaseModel):
validators=[FieldDefinitionValidator(core_fields=FINDING_FIELDS_CORE, predefined_fields=FINDING_FIELDS_PREDEFINED)],
default=finding_fields_default)
finding_field_order = models.JSONField(encoder=DjangoJSONEncoder, default=finding_field_order_default)
finding_ordering = models.JSONField(encoder=DjangoJSONEncoder, validators=[FindingOrderingValidator()], default=finding_ordering_default)
linked_project = models.ForeignKey(to='PentestProject', on_delete=models.SET_NULL, null=True, blank=True)
linked_user = models.ForeignKey(to=PentestUser, on_delete=models.CASCADE, null=True, blank=True)
@ -90,9 +92,18 @@ class ProjectType(LockableMixin, LanguageMixin, ImportableMixin, BaseModel):
if undefined_fields := set(itertools.chain(*map(lambda s: s['fields'], self.report_sections))) - set(self.report_fields.keys()):
raise ValidationError(_('Unknown fields in section: %(fields)s') % {'fields': list(undefined_fields)})
# Validate finding field order contains only defined fields
# Validate finding field field order contains only defined fields
if undefined_fields := set(self.finding_field_order) - set(self.finding_fields.keys()):
raise ValidationError(_('Unknown fields in finding order: %(fields)s') % {'fields': list(undefined_fields)})
# Validate finding ordering contains only defined fields supported types
unsupported_fields = []
for o in self.finding_ordering:
d = self.finding_fields_obj.get(o['field'])
if not d or d.type in [FieldDataType.LIST, FieldDataType.OBJECT, FieldDataType.USER]:
unsupported_fields.append(o['field'])
if unsupported_fields:
raise ValidationError(_('Unsupported fields in finding ordering: %(fields)s') % {'fields': list(unsupported_fields)})
def save(self, *args, **kwargs):
# Ensure static fields are marked correctly
@ -117,17 +128,23 @@ class ProjectType(LockableMixin, LanguageMixin, ImportableMixin, BaseModel):
}
self.report_sections.append(others_section)
others_section['fields'].extend(missing_fields)
# Remove undefined fields from section definition
# Remove unknown fields from section definition
for section in self.report_sections:
for undefined_field in set(section['fields']) - report_fields:
section['fields'].remove(undefined_field)
for unknown_field in set(section['fields']) - report_fields:
section['fields'].remove(unknown_field)
# Ensure finding order contains all fields
finding_fields = set(self.finding_fields.keys())
self.finding_field_order = remove_duplicates(self.finding_field_order + list(finding_fields))
# Remove undefined fields from finding order
for undefined_field in set(self.finding_field_order) - finding_fields:
self.finding_field_order.remove(undefined_field)
# Remove unknown fields from finding_field_order
for unknown_field in set(self.finding_field_order) - finding_fields:
self.finding_field_order.remove(unknown_field)
# Remove unknown fields from finding_ordering
for ordering_def in list(self.finding_ordering):
d = self.finding_fields_obj.get(ordering_def['field'])
if not d or d.type in [FieldDataType.LIST, FieldDataType.OBJECT, FieldDataType.USER]:
self.finding_ordering.remove(ordering_def)
# Ensure correct structure of report_preview_data
if set(self.changed_fields).intersection({'report_preview_data', 'report_fields', 'finding_fields'}):
@ -167,6 +184,8 @@ class PentestProject(EncryptedCustomFieldsMixin, LanguageMixin, ImportableMixin,
project_type = models.ForeignKey(to='ProjectType', on_delete=models.PROTECT)
imported_members = ArrayField(base_field=models.JSONField(encoder=DjangoJSONEncoder), default=list, blank=True)
override_finding_order = models.BooleanField(default=False)
readonly = models.BooleanField(default=False, db_index=True)
readonly_since = models.DateTimeField(null=True, db_index=True, editable=False)
@ -286,8 +305,9 @@ class PentestFinding(EncryptedCustomFieldsMixin, LockableMixin, BaseModel):
template_id = EncryptedField(base_field=models.UUIDField(null=True, blank=True), null=True, blank=True)
assignee = models.ForeignKey(to=PentestUser, on_delete=models.SET_NULL, null=True, blank=True)
status = models.CharField(max_length=20, choices=ReviewStatus.choices, default=ReviewStatus.IN_PROGRESS, db_index=True)
order = models.PositiveIntegerField(default=0, db_index=True)
objects = models.Manager.from_queryset(querysets.PentestFindingQueryset)()
objects = querysets.PentestFindingManager()
class Meta(BaseModel.Meta):
unique_together = [('project', 'finding_id')]
@ -304,10 +324,6 @@ class PentestFinding(EncryptedCustomFieldsMixin, LockableMixin, BaseModel):
def title(self) -> str:
return self.data.get('title')
@property
def risk_score(self) -> float:
return cvss_utils.calculate_score(self.data.get('cvss'))
def __str__(self) -> str:
return self.title

View File

@ -1,15 +1,15 @@
from django.db import models
from django.contrib.postgres.fields import ArrayField
from reportcreator_api.pentests.customfields.utils import HandleUndefinedFieldsOptions, ensure_defined_structure
from reportcreator_api.pentests.customfields.validators import FieldValuesValidator
from reportcreator_api.utils.models import BaseModel
from reportcreator_api.pentests.customfields.mixins import CustomFieldsMixin
from reportcreator_api.pentests.models.common import LockableMixin, ImportableMixin, ReviewStatus, LanguageMixin
from reportcreator_api.pentests import querysets
from reportcreator_api.pentests import cvss as cvss_utils
from reportcreator_api.pentests.customfields.types import FieldDefinition
from reportcreator_api.pentests.customfields.predefined_fields import FINDING_FIELDS_CORE, FINDING_FIELDS_PREDEFINED
from reportcreator_api.pentests.customfields.predefined_fields import FINDING_FIELDS_CORE
from reportcreator_api.utils.decorators import cache
from reportcreator_api.utils.utils import merge, omit_keys, copy_keys

View File

@ -12,11 +12,10 @@ from django.conf import settings
from django.db import models, transaction
from django.db.models.functions import Coalesce
from django.core.exceptions import ValidationError
from reportcreator_api.archive import crypto
from reportcreator_api.archive.crypto.base import ReadIntoAdapter
from reportcreator_api.archive.crypto.secret_sharing import ShamirLarge
from reportcreator_api.archive.crypto.storage import EncryptedFileAdapter, IterableToFileAdapter
from reportcreator_api.pentests.customfields.predefined_fields import FINDING_FIELDS_CORE, FINDING_FIELDS_PREDEFINED
from reportcreator_api.pentests.customfields.types import FieldOrigin, parse_field_definition
from reportcreator_api.users.models import PentestUser
@ -213,6 +212,35 @@ class PentestFindingQueryset(models.QuerySet):
return self.filter(project__members__user=user)
class PentestFindingManager(models.Manager.from_queryset(PentestFindingQueryset)):
def create(self, project=None, data=None, order=None, **kwargs):
from reportcreator_api.pentests.models import PentestFinding
if project and not order:
order = Coalesce(
models.Subquery(
self.filter(project=project)
.values('project')
.annotate(max_order=models.Max('order'))
.values_list('max_order')),
models.Value(0)
) + models.Value(1)
instance = PentestFinding(project=project, order=order, **kwargs)
if data is not None:
instance.update_data(data)
instance.save()
instance.refresh_from_db()
return instance
def update_order(self, instances, missing_instances=None):
missing_instances = missing_instances or []
findings_sorted = sorted(filter(lambda f: f not in missing_instances, instances), key=lambda f: f.order) + \
sorted(filter(lambda f: f in missing_instances, instances), key=lambda f: f.order)
for idx, f in enumerate(findings_sorted):
f.order = idx + 1
self.bulk_update(instances, ['order'])
class ReportSectionQueryset(models.QuerySet):
def only_permitted(self, user):
if user.is_admin:
@ -236,6 +264,23 @@ class FindingTemplateQueryset(models.QuerySet):
return self \
.annotate(has_language=models.Exists(FindingTemplateTranslation.objects.filter(language=language).filter(template=models.OuterRef('pk')))) \
.order_by('-has_language')
def search(self, search_terms: list[str]):
qs = self
for idx, term in enumerate(search_terms):
qs = qs.annotate(**{
f'search_term_{idx}_matches_tags': models.Case(models.When(tags__icontains=term, then=1.0), default=0.0),
f'search_term_{idx}_matches_title': models.Case(models.When(translations__title__icontains=term, then=1.0), default=0.0),
f'search_term_{idx}_matches_data': models.Case(models.When(translations__custom_fields__icontains=term, then=0.2), default=0.0),
f'search_term_{idx}_rank': models.F(f'search_term_{idx}_matches_tags') + models.F(f'search_term_{idx}_matches_title') + models.F(f'search_term_{idx}_matches_data'),
}) \
.filter(**{f'search_term_{idx}_rank__gt': 0})
qs = qs \
.annotate(search_rank=functools.reduce(operator.add, [models.F(f'search_term_{idx}_rank') for idx in range(len(search_terms))]))
order_by = ('-search_rank',)
if qs.query.order_by == ('-has_language',):
order_by = qs.query.order_by + order_by
return qs.order_by(*order_by)
class FindingTemplateTranslationQueryset(models.QuerySet):

View File

@ -67,7 +67,7 @@ class ProjectTypeDetailSerializer(ProjectTypeShortSerializer):
'copy_of', 'lock_info',
'report_template', 'report_styles', 'report_preview_data',
'report_fields', 'report_sections',
'finding_fields', 'finding_field_order',
'finding_fields', 'finding_field_order', 'finding_ordering',
]
@ -110,6 +110,37 @@ class PentestProjectRelatedField(serializers.PrimaryKeyRelatedField):
return PentestProject.objects.only_permitted(self.context['request'].user)
class ReportSectionSerializer(serializers.ModelSerializer):
id = serializers.CharField(source='section_id', read_only=True)
project = serializers.PrimaryKeyRelatedField(read_only=True)
project_type = ProjectTypeRelatedField(source='project.project_type_id', read_only=True)
label = serializers.CharField(source='section_label', read_only=True)
fields = serializers.ListField(source='section_fields', child=serializers.CharField(), read_only=True)
lock_info = LockInfoSerializer()
assignee = RelatedUserSerializer(required=False, allow_null=True)
class Meta:
model = ReportSection
fields = [
'id', 'label', 'fields', 'project', 'project_type',
'language', 'lock_info', 'assignee', 'status',
]
def get_fields(self):
fields = super().get_fields()
data_field = serializers.DictField()
if self.instance and isinstance(self.instance, ReportSection):
data_field = serializer_from_definition(definition=self.instance.field_definition, **self.get_extra_kwargs().get('data', {}))
return fields | {
'data': data_field
}
def update(self, instance, validated_data):
instance.update_data(validated_data.pop('data', {}))
instance.project.save()
return super().update(instance, validated_data)
class PentestFindingSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(source='finding_id', read_only=True)
project = serializers.PrimaryKeyRelatedField(read_only=True)
@ -122,8 +153,9 @@ class PentestFindingSerializer(serializers.ModelSerializer):
model = PentestFinding
fields = [
'id', 'created', 'updated', 'project', 'project_type',
'language', 'lock_info', 'template', 'assignee', 'status',
'language', 'lock_info', 'template', 'assignee', 'status', 'order',
]
read_only_fields = ['order']
def get_fields(self):
data_field = serializers.DictField()
@ -139,13 +171,11 @@ class PentestFindingSerializer(serializers.ModelSerializer):
definition=self.context['project'].project_type.finding_fields_obj,
handle_undefined=handle_undefined
)
instance = PentestFinding(
return PentestFinding.objects.create(
project=self.context['project'],
data=data,
**validated_data
)
instance.update_data(data)
instance.save()
return instance
def update(self, instance, validated_data):
instance.update_data(validated_data.pop('data', {}))
@ -256,9 +286,8 @@ class ImportedProjectMemberInfoListSerializer(serializers.ListSerializer):
return updated
class PentestProjectSerializer(serializers.ModelSerializer):
class PentestProjectShortSerializer(serializers.ModelSerializer):
project_type = ProjectTypeRelatedField()
force_change_project_type = serializers.BooleanField(required=False, default=False, write_only=True)
members = ProjectMemberInfoSerializer(many=True, required=False)
imported_members = ImportedProjectMemberInfoListSerializer(required=False)
@ -275,12 +304,21 @@ class PentestProjectSerializer(serializers.ModelSerializer):
model = PentestProject
fields = [
'id', 'created', 'updated',
'name', 'project_type', 'force_change_project_type', 'language', 'tags', 'readonly', 'source', 'copy_of',
'name', 'project_type', 'language', 'tags', 'readonly', 'source', 'copy_of', 'override_finding_order',
'members', 'imported_members',
'details', 'findings', 'sections', 'notes', 'images',
]
read_only_fields = ['readonly']
class PentestProjectDetailSerializer(PentestProjectShortSerializer):
sections = ReportSectionSerializer(many=True, read_only=True)
findings = PentestFindingSerializer(many=True, read_only=True)
force_change_project_type = serializers.BooleanField(required=False, default=False, write_only=True)
class Meta(PentestProjectShortSerializer.Meta):
fields = PentestProjectShortSerializer.Meta.fields + ['force_change_project_type']
def validate_project_type(self, value):
if self.instance and self.instance.project_type != value and not self.initial_data.get('force_change_project_type'):
res_finding = check_definitions_compatible(self.instance.project_type.finding_fields_obj, value.finding_fields_obj, path=('finding_fields',))
@ -289,7 +327,7 @@ class PentestProjectSerializer(serializers.ModelSerializer):
raise serializers.ValidationError(['Designs have incompatible field definitions. Converting might result in data loss.'] + res_report[1] + res_finding[1])
return value
@transaction.atomic
def create(self, validated_data):
project_type = validated_data.pop('project_type').copy(linked_user=None, source=SourceEnum.SNAPSHOT, created=timezone.now())
@ -336,36 +374,31 @@ class PentestProjectSerializer(serializers.ModelSerializer):
return instance
class ReportSectionSerializer(serializers.ModelSerializer):
id = serializers.CharField(source='section_id', read_only=True)
project = serializers.PrimaryKeyRelatedField(read_only=True)
project_type = ProjectTypeRelatedField(source='project.project_type_id', read_only=True)
label = serializers.CharField(source='section_label', read_only=True)
fields = serializers.ListField(source='section_fields', child=serializers.CharField(), read_only=True)
lock_info = LockInfoSerializer()
assignee = RelatedUserSerializer(required=False, allow_null=True)
class PentestFindingSortSerializer(serializers.ModelSerializer):
id = serializers.UUIDField(source='finding_id')
class Meta:
model = ReportSection
fields = [
'id', 'label', 'fields', 'project', 'project_type',
'language', 'lock_info', 'assignee', 'status',
]
model = PentestFinding
fields = ['id', 'order']
def validate_id(self, value):
if not next(filter(lambda f: f.finding_id == value, self.parent.instance), None):
raise serializers.ValidationError('Invalid finding id')
return value
class PentestFindingSortListSerializer(serializers.ListSerializer):
child = PentestFindingSortSerializer()
def get_fields(self):
fields = super().get_fields()
data_field = serializers.DictField()
if self.instance and isinstance(self.instance, ReportSection):
data_field = serializer_from_definition(definition=self.instance.field_definition, **self.get_extra_kwargs().get('data', {}))
return fields | {
'data': data_field
}
def update(self, instance, validated_data):
instance.update_data(validated_data.pop('data', {}))
instance.project.save()
return super().update(instance, validated_data)
missing_findings = []
for finding in instance:
if data := next(filter(lambda d: finding.finding_id == d.get('finding_id'), validated_data), None):
finding.order = data.get('order')
else:
missing_findings.append(finding)
PentestFinding.objects.update_order(instance, missing_findings)
return instance
class FindingTemplateTranslationShortDataSerializer(serializers.Serializer):
title = serializers.CharField()

View File

@ -11,6 +11,7 @@ from django.db import transaction
from django.db.models import Prefetch, Q, ProtectedError
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.template import loader
from rest_framework import views, viewsets, mixins, status, exceptions
from rest_framework.response import Response
from rest_framework.decorators import action
@ -35,9 +36,9 @@ from reportcreator_api.tasks.rendering.entry import render_pdf, render_pdf_previ
from reportcreator_api.pentests.serializers import ArchivedProjectKeyPartDecryptSerializer, ArchivedProjectKeyPartSerializer, \
ArchivedProjectPublicKeyEncryptedKeyPartSerializer, ArchivedProjectSerializer, CopySerializer, CustomizeProjectTypeSerializer, ExportSerializer, \
FindingTemplateSerializer, FindingTemplateShortSerializer, FindingTemplateTranslationSerializer, ImportSerializer, LockableObjectSerializer, \
NotebookPageSerializer, PdfResponseSerializer, PentestFindingFromTemplateSerializer, PentestFindingSerializer, \
PentestProjectCheckArchiveSerializer, PentestProjectCheckSerializer, PentestProjectCreateArchiveSerializer, \
PentestProjectReadonlySerializer, PentestProjectSerializer, PreviewPdfOptionsSerializer, \
NotebookPageSerializer, PdfResponseSerializer, PentestFindingFromTemplateSerializer, PentestFindingSerializer, PentestFindingSortListSerializer, \
PentestProjectCheckArchiveSerializer, PentestProjectCheckSerializer, PentestProjectCreateArchiveSerializer, PentestProjectDetailSerializer, \
PentestProjectReadonlySerializer, PentestProjectShortSerializer, PreviewPdfOptionsSerializer, \
ProjectNotebookPageCreateSerializer, NotebookPageSortListSerializer, ProjectTypeCreateSerializer, ProjectTypeDetailSerializer, ProjectTypeImportSerializer, \
ProjectTypePreviewSerializer, ProjectTypeShortSerializer, ProjectTypeCopySerializer, PublishPdfOptionsSerializer, ReportSectionSerializer, \
UploadedAssetSerializer, UploadedImageSerializer, PentestProjectCopySerializer, UploadedProjectFileSerilaizer, UploadedUserNotebookFileSerilaizer, \
@ -304,12 +305,14 @@ class ProjectTypePreviewView(ProjectTypeViewSetBase, GenericAPIViewAsync):
@extend_schema(parameters=[OpenApiParameter(name='id', type=UUID, location=OpenApiParameter.PATH)])
class PentestProjectViewSetBase(views.APIView):
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [ProjectPermissions]
serializer_class = PentestProjectSerializer
serializer_class = PentestProjectDetailSerializer
filter_backends = [SearchFilter, DjangoFilterBackend]
search_fields = ['name', 'tags', 'language']
filterset_fields = ['language', 'readonly']
def get_serializer_class(self):
if self.action == 'list':
return PentestProjectShortSerializer
if self.action == 'generate':
return PublishPdfOptionsSerializer
elif self.action == 'preview':
@ -442,6 +445,8 @@ class PentestFindingViewSet(ProjectSubresourceMixin, LockableViewSetMixin, views
def get_serializer_class(self):
if self.action == 'fromtemplate':
return PentestFindingFromTemplateSerializer
elif self.action == 'sort':
return PentestFindingSortListSerializer
return super().get_serializer_class()
def get_queryset(self):
@ -451,6 +456,14 @@ class PentestFindingViewSet(ProjectSubresourceMixin, LockableViewSetMixin, views
@action(detail=False, methods=['post'])
def fromtemplate(self, request, *args, **kwargs):
return super().create(request, *args, **kwargs)
@action(detail=False, methods=['post'])
@transaction.atomic
def sort(self, request, *arg, **kwargs):
serializer = self.get_serializer(instance=list(self.get_queryset()), data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(data=serializer.data)
class ReportSectionViewSet(ProjectSubresourceMixin, LockableViewSetMixin, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
@ -716,6 +729,24 @@ class UploadedAssetViewSet(UploadedFileViewSetMixin, viewsets.ModelViewSet):
}
class FindingTemplateSearchFilter(SearchFilter):
def filter_queryset(self, request, queryset, view):
search_terms = self.get_search_terms(request)
if not search_terms:
return queryset
return queryset \
.search(search_terms)
def to_html(self, request, queryset, view):
context = {
'param': self.search_param,
'term': ' '.join(self.get_search_terms(request))
}
template = loader.get_template(self.template)
return template.render(context)
class FindingTemplateOrderingFilter(OrderingFilter):
ordering_fields = ['risk', 'usage']
@ -725,8 +756,8 @@ class FindingTemplateOrderingFilter(OrderingFilter):
# Combine with preferred_language ordering filter
ordering = []
existing_ordering = list(queryset.query.order_by)
if '-has_language' in existing_ordering:
ordering = ['-has_language']
if existing_ordering in [['-has_language', '-search_rank'], ['-has_language'], ['-search_rank']]:
ordering = existing_ordering
if ordering_query == 'risk':
return ordering + ['main_translation__risk_score', 'created']
@ -761,8 +792,7 @@ class FindingTemplateFilter(FilterSet):
class FindingTemplateViewSet(LockableViewSetMixin, ExportImportViewSetMixin, viewsets.ModelViewSet):
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES + [IsTemplateEditorOrReadOnly]
serializer_class = FindingTemplateSerializer
filter_backends = [SearchFilter, DjangoFilterBackend, FindingTemplateOrderingFilter]
search_fields = ['tags', 'translations__title']
filter_backends = [DjangoFilterBackend, FindingTemplateSearchFilter, FindingTemplateOrderingFilter]
filterset_class = FindingTemplateFilter
pagination_class = CursorMultiPagination

View File

@ -9,6 +9,7 @@ from typing import Any, Optional, Union
from base64 import b64encode
from django.core.exceptions import ValidationError
from django.urls import reverse
from reportcreator_api.pentests.customfields.sort import sort_findings
from reportcreator_api.tasks.rendering import tasks
from reportcreator_api.pentests import cvss
@ -62,18 +63,13 @@ def format_template_field(value: Any, definition: FieldDefinition, members: Opti
if value_type == FieldDataType.ENUM:
return dataclasses.asdict(next(filter(lambda c: c.value == value, definition.choices), EnumChoice(value='', label='')))
elif value_type == FieldDataType.CVSS:
score_metrics = cvss.calculate_score(
value, return_metrics=True)
score_metrics = cvss.calculate_metrics(value)
return {
'vector': value,
'score': str(round(score_metrics["final"]["score"], 2)),
'level': cvss.level_from_score(score_metrics["final"]["score"]).value,
'level_number': cvss.level_number_from_score(score_metrics["final"]["score"]),
'version': score_metrics["version"],
'final': score_metrics["final"],
'base': score_metrics["base"],
'temporal': score_metrics["temporal"],
'environmental': score_metrics["environmental"],
**score_metrics
}
elif value_type == FieldDataType.USER:
return format_template_field_user(value, members=members)
@ -85,7 +81,7 @@ def format_template_field(value: Any, definition: FieldDefinition, members: Opti
return value
def format_template_data(data: dict, project_type: ProjectType, imported_members: Optional[list[dict]] = None):
def format_template_data(data: dict, project_type: ProjectType, imported_members: Optional[list[dict]] = None, override_finding_order=False):
members = [format_template_field_user(u, members=imported_members) for u in data.get(
'pentesters', []) + (imported_members or [])]
data['report'] = format_template_field_object(
@ -96,7 +92,7 @@ def format_template_data(data: dict, project_type: ProjectType, imported_members
definition=project_type.report_fields_obj,
members=members,
require_id=True)
data['findings'] = sorted([
data['findings'] = sort_findings(findings=[
format_template_field_object(
value=(f if isinstance(f, dict) else {}) | ensure_defined_structure(
value=f,
@ -106,7 +102,7 @@ def format_template_data(data: dict, project_type: ProjectType, imported_members
members=members,
require_id=True)
for f in data.get('findings', [])],
key=lambda f: (-float(f.get('cvss', {}).get('score', 0)), f.get('created'), f.get('id')))
project_type=project_type, override_finding_order=override_finding_order)
data['pentesters'] = sorted(
members,
key=lambda u: (0 if 'lead' in u.get('roles', []) else 1 if 'pentester' in u.get(
@ -191,11 +187,17 @@ async def render_pdf(project: PentestProject, project_type: Optional[ProjectType
'findings': [{
'id': str(f.finding_id),
'created': str(f.created),
'order': f.order,
**f.data,
} async for f in project.findings.all()],
'pentesters': [u async for u in project.members.all()],
}
data = await sync_to_async(format_template_data)(data=data, project_type=project_type, imported_members=project.imported_members)
data = await sync_to_async(format_template_data)(
data=data,
project_type=project_type,
imported_members=project.imported_members,
override_finding_order=project.override_finding_order
)
return await render_pdf_task(
project=project,
project_type=project_type,

View File

@ -154,14 +154,12 @@ def create_finding(project, template=None, **kwargs) -> PentestFinding:
handle_undefined=HandleUndefinedFieldsOptions.FILL_DEFAULT,
include_unknown=True,
) | kwargs.pop('data', {})
finding = PentestFinding.objects.create(**{
return PentestFinding.objects.create(**{
'project': project,
'assignee': None,
'template_id': template.id if template else None,
'data': data,
} | kwargs)
finding.update_data(data)
finding.save()
return finding
def create_notebookpage(**kwargs) -> NotebookPage:

View File

@ -99,7 +99,8 @@ def project_viewset_urls(get_obj, read=False, write=False, create=False, list=Fa
if write:
out.extend([
('pentestproject finding-fromtemplate', lambda s, c: c.post(reverse('finding-fromtemplate', kwargs={'project_pk': get_obj(s).pk}), data={'template': s.template.pk})),
('projectnotebookpage sort', lambda s, c: c.post(reverse('projectnotebookpage-sort', kwargs={'project_pk': get_obj(s).pk}), data=[])),
('finding sort', lambda s, c: c.post(reverse('finding-sort', kwargs={'project_pk': get_obj(s).pk}), data=[{'id': get_obj(s).findings.first().finding_id, 'order': 1}])),
('projectnotebookpage sort', lambda s, c: c.post(reverse('projectnotebookpage-sort', kwargs={'project_pk': get_obj(s).pk}), data=[{'id': get_obj(s).notes.first().note_id, 'parent': None, 'order': 1}])),
('pentestproject upload-image-or-file', lambda s, c: c.post(reverse('pentestproject-upload-image-or-file', kwargs={'pk': get_obj(s).pk}), data={'name': 'image.png', 'file': ContentFile(name='image.png', content=create_png_file())}, format='multipart')),
('pentestproject upload-image-or-file', lambda s, c: c.post(reverse('pentestproject-upload-image-or-file', kwargs={'pk': get_obj(s).pk}), data={'name': 'test.pdf', 'file': ContentFile(name='text.pdf', content=b'text')}, format='multipart')),
])

View File

@ -6,7 +6,7 @@ import pytest
from reportcreator_api.archive.import_export.import_export import export_project_types
from reportcreator_api.pentests.cvss import CVSSLevel
from reportcreator_api.pentests.models import ProjectType, ProjectTypeScope, SourceEnum, FindingTemplate, FindingTemplateTranslation, Language, ReviewStatus
from reportcreator_api.tests.mock import create_project, create_project_type, create_template, create_user, api_client
from reportcreator_api.tests.mock import create_project, create_project_type, create_template, create_user, create_notebookpage, api_client
from reportcreator_api.tests.utils import assertKeysEqual
@ -79,6 +79,27 @@ class TestProjectApi:
assert project.imported_members[0]['roles'] == ['pentester']
assert project.imported_members[0]['additional_field'] == 'test'
def test_sort_findings(self):
project = create_project(members=[self.user], findings_kwargs=[
{'data': {'title': 'Finding 1'}, 'order': 5},
{'data': {'title': 'Finding 2'}, 'order': 4},
{'data': {'title': 'Finding 3'}, 'order': 1},
{'data': {'title': 'Finding 4'}, 'order': 2},
{'data': {'title': 'Finding 5'}, 'order': 3}
])
def finding_by_title(title):
return next(filter(lambda f: f.data['title'] == title, project.findings.all()))
res = self.client.post(reverse('finding-sort', kwargs={'project_pk': project.id}), data=[
{'id': finding_by_title('Finding 1').finding_id, 'order': 1},
{'id': finding_by_title('Finding 2').finding_id, 'order': 2},
{'id': finding_by_title('Finding 3').finding_id, 'order': 3},
])
assert res.status_code == 200
expected_order = [f'Finding {i + 1}' for i in range(5)]
assert [project.findings.get(finding_id=f['id']).data['title'] for f in sorted(res.data, key=lambda f: f['order'])] == expected_order
assert [f.data['title'] for f in project.findings.order_by('order')] == expected_order
@pytest.mark.django_db
class TestProjectTypeApi:
@ -322,3 +343,87 @@ class TestTemplateApi:
assert img.name == 'image.png'
assert img.file.read() == project.images.filter_name(img.name).get().file.read()
def assert_search_result(self, params, expected_result):
res = self.client.get(reverse('findingtemplate-list'), data=params)
assert res.status_code == 200
assert [t['id'] for t in res.data['results']] == [str(t.id) for t in expected_result]
def test_template_search(self):
search_term = 'tls crypt'
t_title_tag_data_de = create_template(language=Language.GERMAN, data={'title': 'Weak TLS', 'description': 'Weak crypto'}, tags=['crypto'])
t_title_data_en_de = create_template(language=Language.ENGLISH, data={'title': 'Weak TLS', 'description': 'Weak crypto'},
translations_kwargs=[{'language': Language.GERMAN, 'data': {'title': 'Unrelated', 'description': 'Weak crypto'}}])
t_data_en = create_template(language=Language.ENGLISH, data={'title': 'Unrelated', 'description': 'Improve TLS encryption'})
t_partial_term_match = create_template(language=Language.GERMAN, data={'title': 'Unrelated', 'description': 'Improve TLS'})
t_no_match = create_template(language=Language.GERMAN, data={'title': 'Unrelated', 'description': 'Unrelated'})
# Best match first ordered by search rank
self.assert_search_result({'search': search_term}, [t_title_tag_data_de, t_title_data_en_de, t_data_en])
# Templates of preferred language first, then other languages, ordered by search rank
self.assert_search_result({'search': search_term, 'preferred_language': Language.ENGLISH}, [t_title_data_en_de, t_data_en, t_title_tag_data_de])
# Only templates of language, ordered by search rank
self.assert_search_result({'search': search_term, 'language': Language.ENGLISH}, [t_title_data_en_de, t_data_en])
# All templates
self.assert_search_result({}, [t_no_match, t_partial_term_match, t_data_en, t_title_data_en_de, t_title_tag_data_de, self.template])
@pytest.mark.django_db
class TestNotesApi:
@pytest.fixture(autouse=True)
def setUp(self):
self.user = create_user()
self.client = api_client(self.user)
def test_sort(self):
note1 = create_notebookpage(user=self.user, parent=None, order=3)
top_level = [
note1,
create_notebookpage(user=self.user, parent=None, order=1),
create_notebookpage(user=self.user, parent=None, order=2),
]
sub_level = [
create_notebookpage(user=self.user, parent=note1, order=2),
create_notebookpage(user=self.user, parent=note1, order=3),
create_notebookpage(user=self.user, parent=note1, order=1),
]
res = self.client.post(reverse('usernotebookpage-sort', kwargs={'pentestuser_pk': 'self'}), data=
[{'id': n.note_id, 'parent': None, 'order': idx + 1} for idx, n in enumerate(top_level)] +
[{'id': n.note_id, 'parent': n.parent.note_id, 'order': idx + 1} for idx, n in enumerate(sub_level)]
)
for idx, n in enumerate(top_level):
n.refresh_from_db()
assert n.parent is None
assert n.order == idx + 1
assert next(filter(lambda rn: rn['id'] == str(n.note_id), res.data))['order'] == n.order
for idx, n in enumerate(sub_level):
n.refresh_from_db()
assert n.parent == note1
assert n.order == idx + 1
assert next(filter(lambda rn: rn['id'] == str(n.note_id), res.data))['order'] == n.order
def test_sort_change_parent(self):
note1 = create_notebookpage(user=self.user, parent=None, order=1)
note2 = create_notebookpage(user=self.user, parent=None, order=2)
note1_1 = create_notebookpage(user=self.user, parent=note2, order=1)
note1_2 = create_notebookpage(user=self.user, parent=None, order=3)
note2_1 = create_notebookpage(user=self.user, parent=note1, order=1)
note3 = create_notebookpage(user=self.user, parent=note1, order=2)
res = self.client.post(reverse('usernotebookpage-sort', kwargs={'pentestuser_pk': 'self'}), data=[
{'id': note1.note_id, 'parent': None, 'order': 1},
{'id': note1_1.note_id, 'parent': note1.note_id, 'order': 1},
{'id': note1_2.note_id, 'parent': note1.note_id, 'order': 2},
{'id': note2.note_id, 'parent': None, 'order': 2},
{'id': note2_1.note_id, 'parent': note2.note_id, 'order': 1},
{'id': note3.note_id, 'parent': None, 'order': 3},
])
assert res.status_code == 200
for n in [note1, note2, note3, note1_1, note1_2, note2_1]:
n.refresh_from_db()
assert note1.parent is None
assert note2.parent is None
assert note3.parent is None
assert note1_1.parent == note1
assert note1_2.parent == note1
assert note2_1.parent == note2

View File

@ -1,14 +1,17 @@
from datetime import timedelta
import itertools
import pytest
from django.test import override_settings
from django.core.exceptions import ValidationError
from django.utils import timezone
from reportcreator_api.pentests.customfields.mixins import CustomFieldsMixin
from reportcreator_api.pentests.customfields.predefined_fields import FINDING_FIELDS_CORE, FINDING_FIELDS_PREDEFINED, REPORT_FIELDS_CORE, finding_fields_default
from reportcreator_api.pentests.customfields.sort import sort_findings
from reportcreator_api.pentests.customfields.types import FieldDataType, field_definition_to_dict, parse_field_definition
from reportcreator_api.pentests.customfields.validators import FieldDefinitionValidator, FieldValuesValidator
from reportcreator_api.pentests.customfields.utils import check_definitions_compatible
from reportcreator_api.pentests.models import FindingTemplate, FindingTemplateTranslation, Language
from reportcreator_api.tasks.rendering.entry import format_template_field_object
from reportcreator_api.tests.mock import create_finding, create_project_type, create_project, create_template, create_user
from reportcreator_api.utils.utils import copy_keys
@ -540,3 +543,62 @@ class TestTemplateTranslation:
assert 'recommendation' not in data_inherited
assert data_inherited['field_unknown'] == 'unknown'
assert 'field_unknown' not in self.trans.data
@pytest.mark.django_db
class TestFindingSorting:
def assert_finding_order(self, findings_kwargs, **project_kwargs):
findings_kwargs = reversed(self.format_findings_kwargs(findings_kwargs))
project = create_project(
findings_kwargs=findings_kwargs,
**project_kwargs)
findings_sorted = sort_findings(
findings=[format_template_field_object(
{'id': str(f.id), 'created': str(f.created), 'order': f.order, **f.data},
definition=project.project_type.finding_fields_obj)
for f in project.findings.all()],
project_type=project.project_type,
override_finding_order=project.override_finding_order
)
findings_sorted_titles = [f['title'] for f in findings_sorted]
assert findings_sorted_titles == [f'f{i + 1}' for i in range(len(findings_sorted_titles))]
def format_findings_kwargs(self, findings_kwargs):
for idx, finding_kwarg in enumerate(findings_kwargs):
finding_kwarg.setdefault('data', {})
finding_kwarg['data']['title'] = f'f{idx + 1}'
return findings_kwargs
def test_override_finding_order(self):
self.assert_finding_order(override_finding_order=True, findings_kwargs=[
{'order': 1},
{'order': 2},
{'order': 3}
])
def test_fallback_order(self):
self.assert_finding_order(
override_finding_order=False,
project_type=create_project_type(finding_ordering=[]),
findings_kwargs=[
{'created': timezone.now() - timedelta(days=2)},
{'created': timezone.now() - timedelta(days=1)},
{'created': timezone.now() - timedelta(days=0)}
])
@pytest.mark.parametrize(['finding_ordering', 'findings_kwargs'], [
([{'field': 'cvss', 'order': 'desc'}], [{'cvss': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H'}, {'cvss': 'CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L'}, {'cvss': None}]), # CVSS
([{'field': 'field_string', 'order': 'asc'}], [{'field_string': 'aaa'}, {'field_string': 'bbb'}, {'field_string': 'ccc'}]), # string field
([{'field': 'field_int', 'order': 'asc'}], [{'field_int': 1}, {'field_int': 10}, {'field_int': 13}]), # number
([{'field': 'field_enum', 'order': 'asc'}], [{'field_enum': 'enum1'}, {'field_enum': 'enum2'}]), # enum
([{'field': 'field_date', 'order': 'asc'}], [{'field_date': None}, {'field_date': '2023-01-01'}, {'field_date': '2023-06-01'}]), # date
([{'field': 'field_string', 'order': 'asc'}, {'field': 'field_markdown', 'order': 'asc'}], [{'field_string': 'aaa', 'field_markdown': 'xxx'}, {'field_string': 'aaa', 'field_markdown': 'yyy'}, {'field_string': 'bbb', 'field_markdown': 'zzz'}]), # multiple fields: string, markdown
([{'field': 'field_bool', 'order': 'desc'}, {'field': 'cvss', 'order': 'desc'}], [{'field_bool': True, 'cvss': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H'}, {'field_bool': True, 'cvss': 'CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:C/C:L/I:L/A:L'}, {'field_bool': False, 'cvss': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H'}]), # multiple fields: -bool, -cvss
([{'field': 'field_enum', 'order': 'asc'}, {'field': 'field_int', 'order': 'desc'}], [{'field_enum': 'enum1', 'field_int': 2}, {'field_enum': 'enum1', 'field_int': 1}, {'field_enum': 'enum2', 'field_int': 10}, {'field_enum': 'enum2', 'field_int': 9}]), # multiple fields with mixed asc/desc: enum, -number
])
def test_finding_order_by_fields(self, finding_ordering, findings_kwargs):
self.assert_finding_order(
override_finding_order=False,
project_type=create_project_type(finding_ordering=finding_ordering),
findings_kwargs=[{'data': f} for f in findings_kwargs]
)

View File

@ -282,4 +282,4 @@ def test_cvss(vector, score):
}),
])
def test_cvss_metrics(vector, metrics):
assert cvss.calculate_score(vector, return_metrics=True) == metrics
assert cvss.calculate_metrics(vector) == metrics

View File

@ -118,17 +118,16 @@ class TestImportExport:
assertKeysEqual(t, self.project_type, [
'created', 'name', 'language',
'report_fields', 'report_sections', 'finding_fields', 'finding_field_order',
'report_fields', 'report_sections',
'finding_fields', 'finding_field_order', 'finding_ordering',
'report_template', 'report_styles', 'report_preview_data'])
assert t.source == SourceEnum.IMPORTED
assert {(a.name, a.file.read()) for a in t.assets.all()} == {(a.name, a.file.read()) for a in self.project_type.assets.all()}
def assert_export_import_project(self, project, p):
assertKeysEqual(p, project, ['name', 'language', 'tags'])
assertKeysEqual(p, project, ['name', 'language', 'tags', 'data', 'override_finding_ordering', 'data_all'])
assert members_equal(p.members, project.members)
assert p.data == project.data
assert p.data_all == project.data_all
assert p.source == SourceEnum.IMPORTED
assert p.sections.count() == project.sections.count()
@ -137,13 +136,14 @@ class TestImportExport:
assert p.findings.count() == project.findings.count()
for i, s in zip(p.findings.order_by('finding_id'), project.findings.order_by('finding_id')):
assertKeysEqual(i, s, ['finding_id', 'created', 'assignee', 'status', 'template', 'data', 'data_all'])
assertKeysEqual(i, s, ['finding_id', 'created', 'assignee', 'status', 'order', 'template', 'data', 'data_all'])
assert {(i.name, i.file.read()) for i in p.images.all()} == {(i.name, i.file.read()) for i in project.images.all()}
assertKeysEqual(p.project_type, project.project_type, [
'created', 'name', 'language',
'report_fields', 'report_sections', 'finding_fields', 'finding_field_order',
'report_fields', 'report_sections',
'finding_fields', 'finding_field_order', 'finding_ordering',
'report_template', 'report_styles', 'report_preview_data'])
assert p.project_type.source == SourceEnum.IMPORTED_DEPENDENCY
assert p.project_type.linked_project == p
@ -192,7 +192,7 @@ class TestImportExport:
# Check UUID of nonexistent user is still present in data
assert p.data_all == self.project.data_all
for i, s in zip(p.findings.order_by('created'), self.project.findings.order_by('created')):
assertKeysEqual(i, s, ['finding_id', 'created', 'assignee', 'template', 'data', 'data_all'])
assertKeysEqual(i, s, ['finding_id', 'created', 'assignee', 'status', 'order', 'template', 'data', 'data_all'])
# Test nonexistent user is added to project.imported_members
assert len(p.imported_members) == 1
@ -322,7 +322,8 @@ class TestCopyModel:
assertKeysEqual(pt, cp, {
'name', 'language', 'linked_project',
'report_template', 'report_styles', 'report_preview_data',
'report_fields', 'report_sections', 'finding_fields', 'finding_field_order',
'report_fields', 'report_sections',
'finding_fields', 'finding_field_order', 'finding_ordering',
} - set(exclude_fields))
assert set(pt.assets.values_list('id', flat=True)).intersection(cp.assets.values_list('id', flat=True)) == set()
@ -341,7 +342,7 @@ class TestCopyModel:
assert cp.copy_of == p
assert not cp.readonly
assertKeysEqual(p, cp, [
'name', 'source', 'language', 'tags', 'imported_members', 'data_all'
'name', 'source', 'language', 'tags', 'override_finding_ordering', 'imported_members', 'data_all'
])
self.assert_project_type_copy_equal(p.project_type, cp.project_type, exclude_fields=['source', 'linked_project'])
assert cp.project_type.source == SourceEnum.SNAPSHOT
@ -356,17 +357,17 @@ class TestCopyModel:
for p_s, cp_s in zip(p.sections.order_by('section_id'), cp.sections.order_by('section_id')):
assert p_s != cp_s
assertKeysEqual(p_s, cp_s, ['section_id', 'assignee', 'data'])
assertKeysEqual(p_s, cp_s, ['section_id', 'assignee', 'status', 'data'])
assert not cp_s.is_locked
for p_f, cp_f in zip(p.findings.order_by('finding_id'), cp.findings.order_by('finding_id')):
assert p_f != cp_f
assertKeysEqual(p_f, cp_f, ['finding_id', 'assignee', 'data', 'template'])
assertKeysEqual(p_f, cp_f, ['finding_id', 'assignee', 'status', 'order', 'data', 'template'])
assert not cp_f.is_locked
for p_n, cp_n in zip(p.notes.order_by('note_id'), cp.notes.order_by('note_id')):
assert p_n != cp_n
assertKeysEqual(p_n, cp_n, ['note_id', 'title', 'text', 'emoji', 'order'])
assertKeysEqual(p_n, cp_n, ['note_id', 'title', 'text', 'checked', 'icon_emoji', 'status_emoji', 'order'])
assert not cp_f.is_locked
if p_n.parent:
assert p_n.parent.note_id == cp_n.parent.note_id

View File

@ -12,6 +12,7 @@ from reportcreator_api.tests.mock import create_imported_member, create_project_
from reportcreator_api.tasks.rendering.entry import render_pdf
from reportcreator_api.tasks.rendering.render import render_to_html
from reportcreator_api.utils.utils import merge
from reportcreator_api.pentests import cvss
@pytest.mark.django_db
@ -55,7 +56,7 @@ class TestHtmlRendering:
('{{ report.field_int }}', lambda self: str(self.project.data['field_int'])),
('{{ report.field_enum.value }}', lambda self: self.project.data['field_enum']),
('{{ findings[0].cvss.vector }}', lambda self: self.finding.data['cvss']),
('{{ findings[0].cvss.score }}', lambda self: str(self.finding.risk_score)),
('{{ findings[0].cvss.score }}', lambda self: str(cvss.calculate_score(self.finding.data['cvss']))),
('{{ data.pentesters[0].name }}', lambda self: self.project.imported_members[0]['name']),
('<template v-for="r in data.pentesters[0].roles">{{ r }}</template>', lambda self: ''.join(self.project.imported_members[0]['roles'])),
('{{ data.pentesters[1].name }}', lambda self: self.user.name),

0
api/src/static/.gitkeep Normal file
View File

View File

@ -5,7 +5,7 @@ services:
app:
build:
args:
CA_CERTIFICATES: ${SYSREPTOR_CA_CERTIFICATES}
CA_CERTIFICATES: ${SYSREPTOR_CA_CERTIFICATES:-""}
environment:
SPELLCHECK_URL: http://languagetool:8010/
depends_on:
@ -15,7 +15,7 @@ services:
build:
context: ../languagetool
args:
CA_CERTIFICATES: ${SYSREPTOR_CA_CERTIFICATES}
CA_CERTIFICATES: ${SYSREPTOR_CA_CERTIFICATES:-""}
container_name: 'sysreptor-languagetool'
init: true
environment:

1
dev/app.env.example Normal file
View File

@ -0,0 +1 @@
BACKUP_KEY=""

188
dev/docker-compose.yml Normal file
View File

@ -0,0 +1,188 @@
version: '3.9'
name: sysreptor-dev
services:
db:
image: 'postgres:14'
environment:
POSTGRES_USER: reportcreator
POSTGRES_PASSWORD: reportcreator
POSTGRES_DB: reportcreator
PGDATA: /data
volumes:
- type: volume
source: db-data
target: /data
expose:
- 5432
healthcheck:
test: ["CMD-SHELL", "pg_isready -U reportcreator"]
interval: 2s
timeout: 5s
retries: 30
stop_grace_period: 120s
rabbitmq:
image: rabbitmq:3
hostname: rabbitmq
environment:
RABBITMQ_DEFAULT_USER: reportcreator
RABBITMQ_DEFAULT_PASS: reportcreator
volumes:
- type: volume
source: mq-data
target: /var/lib/rabbitmq
expose:
- 5672
healthcheck:
test: ["CMD-SHELL", "rabbitmq-diagnostics check_port_connectivity"]
interval: 2s
timeout: 5s
retries: 30
pdfviewer-builder:
build:
context: ..
target: pdfviewer-dev
command: npm run build-watch
volumes:
- type: bind
source: ../packages/pdfviewer/
target: /app/packages/pdfviewer/
rendering-builder:
build:
context: ..
target: rendering-dev
command: npm run build-watch
volumes:
- type: bind
source: ../rendering/
target: /app/rendering/
- type: bind
source: ../packages/
target: /app/packages/
rendering-worker:
build:
context: ..
target: api-dev
user: "1000"
command: watchmedo auto-restart --directory=./ --pattern=*.py --recursive -- celery --app=reportcreator_api.conf.celery --quiet worker -Q rendering --without-heartbeat --without-gossip --without-mingle
environment:
PDF_RENDER_SCRIPT_PATH: /app/rendering/dist/bundle.js
CELERY_BROKER_URL: amqp://reportcreator:reportcreator@rabbitmq:5672/
CELERY_RESULT_BACKEND: reportcreator_api.tasks.rendering.celery_worker:CustomRPCBackend://reportcreator:reportcreator@rabbitmq:5672/
CELERY_SECURE_WORKER: "on"
volumes:
- type: bind
source: ../api/src/
target: /app/api/
- type: bind
source: ../rendering/dist
target: /app/rendering/dist/
depends_on:
rabbitmq:
condition: service_healthy
rendering-builder:
condition: service_started
languagetool:
build:
context: ../languagetool
init: true
environment:
languagetool_dbHost: db
languagetool_dbName: reportcreator
languagetool_dbUsername: reportcreator
languagetool_dbPassword: reportcreator
expose:
- 8010
healthcheck:
test: ["CMD", "curl", "-f", "-so", "/dev/null", "http://localhost:8010/v2/languages"]
interval: 30s
timeout: 30s
retries: 5
start_period: 10s
depends_on:
db:
condition: service_healthy
api:
build:
context: ..
target: api-dev
command: sh -c "python3 manage.py migrate && python3 -m debugpy --listen 0.0.0.0:5678 -m uvicorn --reload --host 0.0.0.0 --port 8000 reportcreator_api.conf.asgi:application"
init: true
volumes:
- type: volume
source: app-data
target: /data
- type: bind
source: ../api/src/
target: /app/api/
- type: bind
source: ../rendering/dist
target: /app/rendering/dist/
expose:
- 8000
ports:
- "8000:8000"
- "5678:5678"
environment:
DEBUG: "on"
DATABASE_HOST: db
DATABASE_NAME: reportcreator
DATABASE_USER: reportcreator
DATABASE_PASSWORD: reportcreator
SPELLCHECK_URL: http://languagetool:8010/
MEDIA_ROOT: /data/
ENCRYPTION_KEYS: '[{"id": "local-dev-key", "key": "t02ZBxs4cl6xi7aSO46PVhUbEUNcRjIeyr0eF/OUQOg=", "cipher": "AES-GCM"}]'
DEFAULT_ENCRYPTION_KEY_ID: local-dev-key
CELERY_BROKER_URL: amqp://reportcreator:reportcreator@rabbitmq:5672/
CELERY_RESULT_BACKEND: reportcreator_api.tasks.rendering.celery_worker:CustomRPCBackend://reportcreator:reportcreator@rabbitmq:5672/
MFA_FIDO2_RP_ID: localhost
PDF_RENDER_SCRIPT_PATH: /app/rendering/dist/bundle.js
env_file: app.env
depends_on:
db:
condition: service_healthy
rendering-worker:
condition: service_started
languagetool:
condition: service_started
frontend:
build:
context: ..
target: frontend-dev
command: npm run dev
volumes:
- type: bind
source: ../frontend/
target: /app/frontend/
- type: bind
source: ../packages/
target: /app/packages/
- type: bind
source: ../packages/pdfviewer/dist/
target: /app/frontend/static/static/pdfviewer/
- type: bind
source: ../api/src/reportcreator_api/tasks/rendering/global_assets/
target: /app/frontend/assets/rendering/
expose:
- 3000
ports:
- "3000:3000"
environment:
HOST: 0.0.0.0
depends_on:
api:
condition: service_started
pdfviewer-builder:
condition: service_started
volumes:
db-data:
name: sysreptor-db-data
mq-data:
name: sysreptor-mq-data
app-data:
name: sysreptor-app-data

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 MiB

View File

@ -5,9 +5,11 @@ echo "Good to see you."
echo "Get ready for the easiest pentest reporting tool."
echo ""
export SYSREPTOR_CA_CERTIFICATES="${SYSREPTOR_CA_CERTIFICATES:-}"
error=1
docker=1
for cmd in curl openssl tar uuidgen docker "docker compose" sed
for cmd in curl openssl tar uuidgen docker sed
do
if
! command -v "$cmd" >/dev/null
@ -21,6 +23,13 @@ do
error=0
fi
done
if
! docker compose version >/dev/null 2>&1
then
echo "docker compose v2 is not installed."
docker=0
error=0
fi
if
test 0 -eq "$docker"
then

1
docs/docs/s/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*

View File

@ -0,0 +1,50 @@
---
title: Generic OIDC Configuration
---
# Generic OIDC Configuration
<span style="color:red;">:octicons-heart-fill-24: Pro only</span>
## Configuration at your OIDC provider
1. Create a `client_id` and a `client_secret` in your OIDC provider
2. Add the callback-url: https://`<your-installation>`/login/oidc/`<your-provider-name>`/callback
* Add the hostname where your SysReptor installation can be accessed.
* Choose a custom provider name (e. g. `keycloak`)
## Cloud Setup
:octicons-cloud-24: Cloud
You are lucky. Just send the values from the previous steps to us and we'll take care :smiling_face_with_3_hearts:
## Self-Hosted Setup
:octicons-server-24: Self-Hosted
Create your OIDC configuration for SysReptor...
```json
{
"<your provider name>": {
"label": "<human readable provider name>",
"client_id": "<your client_id>",
"client_secret": "<your_client_secret>",
"server_metadata_url": "<link to OIDC provider's openid-configuration>",
"client_kwargs": {
"scope": "openid email",
"code_challenge_method": "S256"
},
"reauth_supported": false
}
}
```
```env
OIDC_AUTHLIB_OAUTH_CLIENTS='"<your provider name>": {"label": "<human readable provider name>",...''
```
## Limitations
SysReptor reauthenticates users before critical actions. It therefore requires users to enter their authentication details (e.g. password and second factor, if configured).
Your OIDC provider might not support enforced reauthentication. Your can try to set `"reauth_supported": true`. If the "SUDO" functionality does not work, set to this value to `false`.
To enforce reauthentication, users can set a password for their local SysReptor user. This will enforce reauthentication with the local user's credentials.

View File

@ -0,0 +1,50 @@
---
title: Keycloak OIDC Configuration
---
# Keycloak OIDC Configuration
<span style="color:red;">:octicons-heart-fill-24: Pro only</span>
## Configuration at your OIDC provider
1. Create new Keycloak client for authentication and generate `client_id` and a `client_secret`
2. Add the callback-url: https://`<your-installation>`/login/oidc/keycloak/callback
* Add the hostname where your SysReptor installation can be accessed.
## Cloud Setup
:octicons-cloud-24: Cloud
You are lucky. Just send the values from the previous steps to us and we'll take care :smiling_face_with_3_hearts:
## Self-Hosted Setup
:octicons-server-24: Self-Hosted
Create your OIDC configuration for SysReptor...
```json
{
"keycloak": {
"label": "Keycloak",
"client_id": "<client-id>",
"client_secret": "<client-secret>",
"server_metadata_url": "https://keycloak.example.com/auth/realms/dev/.well-known/openid-configuration",
"client_kwargs": {
"scope": "openid email",
"code_challenge_method": "S256"
},
"reauth_supported": false
}
}
```
...and add it to your `app.env`:
```env
OIDC_AUTHLIB_OAUTH_CLIENTS='"keycloak": {"label": "Keycloak",...''
```
## Limitations
SysReptor reauthenticates users before critical actions. It therefore requires users to enter their authentication details (e.g. password and second factor, if configured).
Your Keycloak installation might not support enforced reauthentication. Your can try to set `"reauth_supported": true`. If the "SUDO" functionality does not work, set to this value to `false`.
To enforce reauthentication, users can set a password for their local SysReptor user. This will enforce reauthentication with the local user's credentials.

View File

@ -4,8 +4,10 @@
1. Configure your Identity Provider (IDP) and add configuration details to your `app.env`
* [Azure Active Directory](/setup/oidc-azure-active-directory)
* [Google Workplace/Google Identity](/setup/oidc-google)
* [Keycloak](/setup/oidc-keycloak)
* [Generic OIDC setup](/setup/oidc-generic)
* Need documentation for another IDP? Drop us a message at [GitHub Discussions](https://github.com/Syslifters/sysreptor/discussions/categories/ideas){ target=_blank }!
3. Restart containers using `docker-compose up -d` in `deploy`-directory
3. Restart containers using `docker-compose up -d` in `deploy` directory
2. Set up local users:
a. Create user that should use SSO

213
docs/hooks.py Executable file
View File

@ -0,0 +1,213 @@
import operator
import yaml
import requests
import os
import sys
SOFTWARE_FILE = 'reporting_software.yml'
DOCUMENT_CONTENT = '''---
{metadata}
search:
exclude: true
---
# {title}
{preface}
<br>
{table}
{postface}
'''
ALTERNATIVE_TO_PREFACE = '''
Similar projects and and alternatives to [{name}]({url}){{target=_blank}} Penetration Test Reporting Tool.
'''
PREFACE = '''
SysReptor is a Pentest Reporting Tool written by pentesters, for pentesters. It is built with security in mind, best usability and strongest focus on the needs of pentesters.
However, if it does not fit your needs, here is a list of alternative tools.
'''
TABLE_HEADER = '''| Name | Report Customization | Deployment | Costs/User/Month |
| - | - | - | - |'''
TABLE_ROW = '''| {software_icon} [{name}]({url}){{target=_blank}} | :material-file-document: {customization} | :material-server: {deployment} | :material-tag: {price} |
'''
POSTFACE = """
<br><div style="text-align:center">[:rocket: Sign Up to SysReptor](#){ .md-button .no-print target="_blank" }</div>
<br>
This overview of penetration testing reporting tools has been compiled to the best of our knowledge and belief. We do not guarantee that the information is correct or up-to-date.
We regard software projects without updates for one year, with missing security patches or major dependencies without support as discontinued.
We welcome tips on other pentest reporting tools.
For inquiries and tips write us a short message to hello@syslifters.com.
"""
def generate_software_lists(*args, **kwargs):
ret = 0
software_list = get_software()
if not kwargs.get('config', dict()).get('site_url'):
# Check links at deployment time
# site_url is empty during gh-deploy, at server it is 127.0.0.1:8000
ret = check_url_availability(software_list)
if not need_regenerate(software_list):
sys.exit(ret)
# Generate "Pentest Reporting Tools" page
title = f"Pentest Reporting Tools - A List of the most popular tools"
metadata = f"title: {title}"
preface = PREFACE
table = generate_table(software_list)
postface = POSTFACE
document = DOCUMENT_CONTENT.format(
metadata=metadata,
title=title,
preface=preface,
table=table,
postface=postface
)
# write document
with open("docs/s/pentest-reporting-tools.md", 'w', encoding='utf-8') as f:
f.write(document)
# Generate "Alternative To Pages"
for software in software_list:
if software.get('self'):
# No alternative to us page
continue
title = f"Alternatives to {software['name']} Pentesting Reporting Tool"
metadata = f'''title: {title.format(name=software['name'])}'''
title = title.format(name=f"**{software['name']}")
preface = ALTERNATIVE_TO_PREFACE.format(
name=software['name'],
url=software['url'],
) + PREFACE
table = generate_table(software_list)
postface = POSTFACE
if not table:
# If no table generated, do not create page
continue
document = DOCUMENT_CONTENT.format(
metadata=metadata,
title=title,
preface=preface,
table=table,
postface=postface
)
# write document
with open(get_filename(software['name']), 'w', encoding='utf-8') as f:
f.write(document)
sys.exit(ret)
def get_filename(name):
replace_chars = [
('/', '-'),
(' ', '-'),
('.', ''),
('ö', 'oe'),
('ä', 'ae'),
('ü', 'ue'),
('ß', 'ss'),
]
name = name.lower()
for replace in replace_chars:
name = name.replace(replace[0], replace[1])
return f'docs/s/alternative-to-{name}-reporting-tool.md'
def need_regenerate(software_list):
oldest_md_mtime = float('inf')
for software in software_list:
try:
oldest_md_mtime = min(os.path.getmtime(
get_filename(software['name'])), oldest_md_mtime)
except FileNotFoundError:
return True
list_mtime = os.path.getmtime(SOFTWARE_FILE)
script_mtime = os.path.getmtime(os.path.realpath(__file__))
if list_mtime > oldest_md_mtime or script_mtime > oldest_md_mtime:
return True
def sort_software(software):
# Filter out empty entries
software_list = [c for c in software if c['name']]
for software in software_list:
if 'self' not in software:
software['self'] = False
if 'discontinued' not in software:
software['discontinued'] = False
if 'url' not in software:
raise KeyError(f"No url specified for {software['name']}")
if 'price' not in software:
raise KeyError(f"No price specified for {software['name']}")
software_list.sort(key=lambda k: (k['name'].lower()))
software_list.sort(key=operator.itemgetter('discontinued'), reverse=False)
software_list.sort(key=operator.itemgetter('self'), reverse=True)
return software_list
def get_software():
# Read all software
with open(SOFTWARE_FILE, 'r', encoding='utf-8') as f:
software = yaml.safe_load(f).get('software')
software = sort_software(software)
return software
def check_url_availability(software_list):
errors = 0
for s in software_list:
try:
r = requests.head(s['url'], timeout=4, headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"})
except requests.exceptions:
errors += 1
print(f"{r.status_code} URL for {s['name']} ist not reachable: {s['url']}")
if r.status_code >= 400:
errors += 1
print(f"{r.status_code} URL for {s['name']} ist not reachable: {s['url']}")
if errors:
return 127
def generate_table(software_list, skip_software=None):
table_rows = list()
for software in software_list:
software_icon = ""
if software.get('self'):
software_icon = "🔥"
elif software.get('discontinued'):
software_icon = ''
cons_icon = ":material-arrow-down-box:" if not software.get('discontinued') else ':octicons-x-circle-fill-12:{ style="color: #e21212;" }'
table_row = TABLE_ROW.format(
software_icon=software_icon,
name=software['name'],
url=software['url'],
pros=software['pros'] if software['pros'] else '',
cons_icon=cons_icon,
cons=software['cons'] if software['cons'] else '',
customization=software['customization'] if software['customization'] else '',
deployment=software['deployment'] if software['deployment'] else '',
price=software['price'] if software['price'] else "",
)
if software['name'] != skip_software:
table_rows.append(table_row)
table = None
if table_rows:
table = f"{TABLE_HEADER}\n{''.join(table_rows)}"
return table

View File

@ -73,15 +73,15 @@ plugins:
archive: false
categories: false
post_readtime: false
#- social:
# cards: !ENV [CARDS, true]
# cards_color:
# fill: "#001827"
# text: "#F9FDFF"
# cards_font: Exo
- social:
cards: !ENV [CARDS, true]
cards_color:
fill: "#001827"
text: "#F9FDFF"
cards_font: Exo
- tooltips
- search
#- privacy
- privacy
- redirects:
redirect_maps:
"setup/nginx-server.md": "setup/webserver.md"

183
docs/reporting_software.yml Executable file
View File

@ -0,0 +1,183 @@
---
might_be_added:
- None
software:
- name:
self: no
url:
discontinued: no
pros:
cons:
customization:
deployment:
price:
- name: vulnrepo
self: no
url: https://vulnrepo.com/
discontinued: no
pros:
cons:
customization: Not provided
deployment: OnPrem
price: Free and Open Source
- name: Vulnreport
self: no
url: https://github.com/salesforce/vulnreport
discontinued: yes
pros:
cons:
customization: Unknown
deployment: OnPrem
price: Free and Open Source
- name: PeTeReport
self: no
url: https://github.com/1modm/petereport
discontinued: no
pros:
cons:
customization: LaTeX/Eisvogel
deployment: OnPrem
price: Free and Open Source
- name: Reporter
self: no
url: https://securityreporter.app/
discontinued: no
pros:
cons:
customization: '"Branded" (possibly not customizable)'
deployment: Cloud/OnPrem
price: From $ 160
- name: PenTest.WS
self: no
url: https://pentest.ws/
discontinued: no
pros:
cons:
customization: docx with custom syntax and HTML
deployment: Cloud
price: From $ 4.95
- name: Faraday
self: no
url: https://faradaysec.com/
discontinued: no
pros:
cons:
customization: docx/Jinja2
deployment: Cloud/OnPrem
price: Free or from $ 120
- name: Canopy
self: no
url: https://www.checksec.com/canopy.html
discontinued: no
pros:
cons:
customization: docx with custom Word plugin
deployment: Cloud/OnPrem
price: Unknown
- name: Ghostwriter
self: no
url: https://github.com/GhostManager/Ghostwriter
discontinued: no
pros:
cons:
customization: docx/Jinja2
deployment: OnPrem
price: Free and Open Source
- name: PlexTrac
self: no
url: https://plextrac.com/
discontinued: no
pros: Large set of predefined finding templates
cons: Intransparent product presentation
customization: docx/Jinja2
deployment: Cloud/OnPrem
price: Top secret
- name: Dradis
self: no
url: https://dradisframework.com/
discontinued: no
pros: Activity feed and audit trail of reporting activity
cons: Cumbersome customizing in MS Word
customization: docx (Dradis optionally customizes for you)
deployment: Cloud/OnPrem
price: Free or $ 79 or $ 149
- name: WriteHat
self: no
url: https://github.com/blacklanternsecurity/writehat
discontinued: yes
pros: Focused on pentest reporting, from pentesters for pentesters
cons: Based on outdated Django version
customization: HTML/Django Templating Language
deployment: OnPrem
price: Free and Open Source
- name: AttackForge
self: no
url: https://attackforge.com/
discontinued: no
pros: Collaboration features
cons: Unintuitive project dashboard
customization: docx with customized template tags
deployment: Cloud or OnPrem (Enterprise only)
price: Free or $ 30 to $ 50
- name: Pwndoc
self: no
url: https://github.com/pwndoc/pwndoc
discontinued: no
pros: Easy and focused on pentest reporting
cons: Cumbersome image upload
customization: docx via docxtemplater
deployment: OnPrem
price: Free and Open Source
- name: Hexway Hive
self: no
url: https://hexway.io/hive/
discontinued: no
pros: Lots of additional features
cons: Rather overloaded for reporting requirements
customization: docx with jinja-like syntax (Hexway customizes for you)
deployment: Cloud/OnPrem
price: Free or $ 78
- name: Reconmap
self: no
url: https://github.com/reconmap/reconmap
discontinued: no
pros: Intuitive, fast results
cons: Limited report customization (no conditials, images, etc)
customization: docx via PHPWord
deployment: OnPrem
price: Free and Open Source
- name: Serpico
url: https://github.com/SerpicoProject/Serpico
discontinued: yes
pros: Fast results
cons: Discontinued, unresolved bugs
customization: docx with custom Meta Language
deployment: OnPrem
price: Free and Open Source
- name: SysReptor
self: yes
url: https://docs.sysreptor.com
discontinued: no
pros: Fastest to get started, Tailored to pentesting needs
cons: Limited permission management in free tier
customization: HTML with VueJS
deployment: Cloud/OnPrem
price: Free or € 50

View File

@ -17,6 +17,11 @@ body {
.login-header {
background-color: $syslifters-darkblue !important;
}
.toast-warning {
// Vuetify warning color
background-color: #fb8c00 !important;
color: black !important;
}
.w-100 {
@ -25,3 +30,14 @@ body {
.h-100 {
height: 100% !important;
}
.flex-grow-height {
flex-grow: 1;
min-height: 0;
}
.flex-grow-width {
flex-grow: 1;
min-width: 0;
}

View File

@ -1,5 +1,5 @@
<template>
<s-dialog v-if="confirm" v-model="confirmDialogVisible" max-width="500">
<s-dialog v-if="confirm" v-model="confirmDialogVisible" :disabled="disabled" max-width="500">
<template #activator="{on: dialogOn, attrs: dialogAttrs}">
<s-tooltip :disabled="!tooltipText">
<template #activator="{on: tooltipOn, attrs: tooltipAttrs}">
@ -70,30 +70,40 @@
</template>
</s-dialog>
<v-list-item
v-else-if="listItem"
@click="performAction"
link
v-bind="$attrs"
>
<v-list-item-icon v-if="buttonIcon">
<v-progress-circular v-if="actionInProgress" indeterminate />
<v-icon v-else :color="$attrs.color || buttonColor">{{ buttonIcon }}</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ buttonText }}</v-list-item-title>
</v-list-item>
<s-btn
v-else
:icon="icon"
:loading="actionInProgress"
@click="performAction"
:color="$attrs.color || buttonColor || 'secondary'"
class="ml-1 mr-1"
v-bind="$attrs"
>
<v-icon v-if="buttonIcon">{{ buttonIcon }}</v-icon>
<template v-if="!icon">{{ buttonText }}</template>
</s-btn>
<s-tooltip v-else :disabled="!tooltipText">
<template #activator="{on: tooltipOn, attrs: tooltipAttrs}">
<v-list-item
v-if="listItem"
@click="performAction"
:disabled="disabled"
link
v-bind="{...tooltipAttrs, ...$attrs}"
v-on="{...tooltipOn, ...$listeners}"
>
<v-list-item-icon v-if="buttonIcon">
<v-progress-circular v-if="actionInProgress" indeterminate />
<v-icon v-else :color="$attrs.color || buttonColor">{{ buttonIcon }}</v-icon>
</v-list-item-icon>
<v-list-item-title>{{ buttonText }}</v-list-item-title>
</v-list-item>
<s-btn
v-else
:icon="icon"
:loading="actionInProgress"
:disabled="disabled"
@click="performAction"
:color="$attrs.color || buttonColor || 'secondary'"
class="ml-1 mr-1"
v-bind="{...tooltipAttrs, ...$attrs}"
v-on="{...tooltipOn, ...$listeners}"
>
<v-icon v-if="buttonIcon">{{ buttonIcon }}</v-icon>
<template v-if="!icon">{{ buttonText }}</template>
</s-btn>
</template>
<template #default>{{ tooltipText }}</template>
</s-tooltip>
</template>
<script>
@ -143,6 +153,14 @@ export default {
type: Function,
required: true,
},
disabled: {
type: Boolean,
default: false,
},
keyboardShortcut: {
type: String,
default: null
},
},
emits: ['click'],
data() {
@ -152,9 +170,27 @@ export default {
actionInProgress: false,
}
},
mounted() {
if (this.keyboardShortcut) {
window.addEventListener('keydown', this.onKeyDown);
}
},
beforeDestroy() {
window.removeEventListener('keydown', this.onKeyDown);
},
methods: {
onKeyDown(event) {
if ((this.keyboardShortcut.startsWith('ctrl+') && event.ctrlKey && event.key === this.keyboardShortcut.substring(5)) || (this.keyboardShortcut === event.key)) {
event.preventDefault();
if (this.confirm) {
this.confirmDialogVisible = true;
} else {
this.performAction();
}
}
},
async performAction() {
if (this.actionInProgress) {
if (this.actionInProgress || this.disabled) {
return;
}

View File

@ -1,26 +1,10 @@
<template>
<v-card
flat tile
class="drag-drop-area"
@drop.prevent="performImport($event.dataTransfer.files)"
@dragover.prevent="showDropArea = true"
@dragenter.prevent="showDropArea = true"
@dragleave.prevent="showDropArea = false"
>
<s-btn @click="$refs.fileInput.click()" :loading="importInProgress" color="primary" v-bind="$attrs">
<v-icon>mdi-upload</v-icon>
Import
</s-btn>
<input ref="fileInput" type="file" @change="performImport($event.target.files)" class="d-none" :disabled="disabled || importInProgress" />
<s-btn @click="$refs.fileInput.click()" :loading="importInProgress" color="primary" v-bind="$attrs">
<v-icon>mdi-upload</v-icon>
Import
<v-fade-transition v-if="!disabled">
<v-overlay v-if="showDropArea" absolute>
<div class="text-center">
Import file
</div>
</v-overlay>
</v-fade-transition>
</v-card>
<input ref="fileInput" type="file" @change="performImport($event.target.files)" class="d-none" :disabled="disabled || importInProgress" />
</s-btn>
</template>
<script>
@ -38,7 +22,6 @@ export default {
data() {
return {
importInProgress: false,
showDropArea: false,
}
},
methods: {
@ -50,7 +33,6 @@ export default {
try {
this.importInProgress = true;
this.showDropArea = false;
await this.import(file);
} catch (error) {
@ -67,10 +49,3 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.drag-drop-area {
display: inline-block;
border-width: 0;
}
</style>

View File

@ -1,10 +1,17 @@
<template>
<s-dialog v-model="dialogVisible">
<template #activator="{ on, attrs }">
<s-btn :disabled="project.readonly" color="secondary" small block v-bind="attrs" v-on="on">
<v-icon>mdi-plus</v-icon>
Create
</s-btn>
<template #activator>
<btn-confirm
:action="() => dialogVisible = true"
:confirm="false"
button-text="Create"
button-icon="mdi-plus"
tooltip-text="Create Finding (Ctrl+J)"
keyboard-shortcut="ctrl+j"
color="secondary"
small
block
/>
</template>
<template #title>New Finding</template>
@ -44,6 +51,7 @@
v-model="templateLanguage"
:items="templateLanguageChoices"
:disabled="!currentTemplate"
class="mt-4"
/>
</v-col>
</v-row>

View File

@ -1,6 +1,6 @@
<template>
<div class="d-flex">
<div class="flex-grow-1">
<div class="flex-grow-width">
<s-text-field
:value="value"
@input="$emit('input', $event)"

View File

@ -1,11 +1,5 @@
<template>
<div
class="drag-drop-area"
@drop.prevent="performFileUpload($event.dataTransfer.files)"
@dragover.prevent="showDropArea = true"
@dragenter.prevent="showDropArea = true"
@dragleave.prevent="showDropArea = false"
>
<file-drop-area multiple :disabled="disabled" @drop="performFileUpload">
<!-- Upload files with drag-and-drop here -->
<v-row class="ma-0">
<v-col :cols="12" :md="3">
@ -73,20 +67,11 @@
</v-card>
</v-col>
</v-row>
<v-fade-transition v-if="!disabled">
<v-overlay v-if="showDropArea" absolute>
<div class="text-center mt-10">
<h2>Drop files to upload</h2>
</div>
</v-overlay>
</v-fade-transition>
</div>
</file-drop-area>
</template>
<script>
import { last } from 'lodash'
import FileDownload from 'js-file-download';
import urlJoin from 'url-join';
import PageLoader from '../PageLoader.vue';
import { uploadFileHelper } from '~/utils/upload';
@ -102,13 +87,12 @@ export default {
disabled: {
type: Boolean,
default: false,
}
},
},
data() {
return {
assets: new CursorPaginationFetcher(`/projecttypes/${this.projectType.id}/assets/`, this.$axios, this.$toast),
uploadInProgress: false,
showDropArea: false,
}
},
computed: {
@ -143,7 +127,6 @@ export default {
try {
this.uploadInProgress = true;
this.showDropArea = false;
// upload all files
await Promise.all(Array.from(files).map(f => this.uploadSingleFile(f)));
@ -170,10 +153,6 @@ export default {
</script>
<style lang="scss" scoped>
.drag-drop-area {
min-height: 100%;
}
.text--small {
font-size: smaller;
}

View File

@ -0,0 +1,129 @@
<template>
<s-card class="mt-4 mb-4">
<v-card-title>Finding Ordering</v-card-title>
<v-card-text>
<p class="mb-0">
Order findings by following fields in reports:
</p>
<v-list>
<draggable
:value="value"
@input="$emit('input', $event)"
:disabled="disabled"
draggable=".draggable-item"
handle=".draggable-handle"
>
<v-list-item v-for="orderConfig, idx in value" :key="idx + '_' + orderConfig.field" dense class="draggable-item">
<v-list-item-icon class="mt-6 mr-0">
<span v-if="idx === 0">Sort by</span>
<span v-else>then by</span>
<v-icon left class="draggable-handle ml-6" :disabled="disabled">mdi-drag</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-row dense>
<v-col cols="6">
<s-select
:value="orderConfig.field"
@input="updateField(idx, orderConfig, $event)"
label="Field"
:items="[{id: orderConfig.field}].concat(availableFindingFields)"
item-text="id"
item-value="id"
:disabled="disabled"
/>
</v-col>
<v-col cols="6">
<s-select
:value="orderConfig.order"
@input="updateOrder(idx, orderConfig, $event)"
label="Order"
:items="['asc', 'desc']"
:disabled="disabled"
/>
</v-col>
</v-row>
</v-list-item-content>
<v-list-item-action>
<btn-delete
:delete="() => deleteOrderConfig(orderConfig)"
:confirm="false"
:disabled="disabled"
icon
/>
</v-list-item-action>
</v-list-item>
</draggable>
<v-list-item>
<s-btn
@click="addField"
:disabled="disabled || availableFindingFields.length === 0"
color="secondary"
>
<v-icon left>mdi-plus</v-icon>
Add
</s-btn>
</v-list-item>
</v-list>
</v-card-text>
</s-card>
</template>
<script>
import Draggable from 'vuedraggable';
export default {
components: { Draggable },
props: {
value: {
type: Array,
required: true,
},
projectType: {
type: Object,
required: true,
},
disabled: {
type: Boolean,
default: false
}
},
emits: ['input'],
computed: {
findingFields() {
return this.projectType.finding_field_order
.map(f => ({ id: f, ...this.projectType.finding_fields[f] }))
.filter(f => !['list', 'object', 'user'].includes(f.type));
},
availableFindingFields() {
return this.findingFields.filter(f => !this.value.map(o => o.field).includes(f.id));
},
},
methods: {
addField() {
this.$emit('input', [...this.value, { field: this.availableFindingFields[0].id, order: 'asc' }]);
},
deleteOrderConfig(orderConfig) {
this.$emit('input', this.value.filter(o => o !== orderConfig));
},
updateField(idx, orderConfig, field) {
const newOrderConfig = [...this.value];
newOrderConfig[idx] = { ...orderConfig, field };
this.$emit('input', newOrderConfig);
},
updateOrder(idx, orderConfig, order) {
const newOrderConfig = [...this.value];
newOrderConfig[idx] = { ...orderConfig, order };
this.$emit('input', newOrderConfig);
},
}
}
</script>
<style lang="scss" scoped>
.draggable-handle {
cursor: grab;
}
</style>

View File

@ -40,7 +40,7 @@
<v-row v-if="![DATA_TYPES.boolean, DATA_TYPES.object].includes(value.type)" class="mt-0">
<v-col class="mt-0 pt-0">
<s-checkbox
:value="value.required"
:value="value.required || false"
@input="emitInputVal('required', $event)"
:disabled="disabled"
label="Required"
@ -50,11 +50,12 @@
</v-col>
<v-col class="mt-0 pt-0" v-if="value.type === DATA_TYPES.string">
<s-checkbox
:value="value.spellcheck"
:value="value.spellcheck || false"
@input="emitInputVal('spellcheck', $event)"
:disabled="disabled"
label="Spellcheck Supported"
hint="Support spellchecking for this fields text content."
class="mt-0"
/>
</v-col>
</v-row>
@ -72,34 +73,45 @@
<!-- Enum choices -->
<v-list v-if="value.type === DATA_TYPES.enum">
<v-list-item v-for="choice, choiceIdx in value.choices || []" :key="choiceIdx">
<v-list-item-content>
<v-row>
<v-col>
<s-text-field
:value="choice.value"
@input="emitInputEnumChoice('updateValue', choice, $event)"
:disabled="disabled || !canChangeStructure"
:rules="rules.choice"
label="Value"
required
/>
</v-col>
<v-col>
<s-text-field
:value="choice.label"
@input="emitInputEnumChoice('updateLabel', choice, $event)"
:disabled="disabled"
label="Label"
required
/>
</v-col>
</v-row>
</v-list-item-content>
<v-list-item-action>
<btn-delete :delete="() => emitInputEnumChoice('delete', choice)" :disabled="disabled || !canChangeStructure" icon />
</v-list-item-action>
</v-list-item>
<draggable
:value="value.choices || []"
@input="emitInputEnumChoice('sort', null, $event)"
:disabled="disabled || !canChangeStructure"
draggable=".draggable-item"
handle=".draggable-handle"
>
<v-list-item v-for="choice, choiceIdx in value.choices || []" :key="choiceIdx" class="draggable-item">
<v-list-item-icon class="draggable-handle mr-0 mt-6">
<v-icon left :disabled="disabled || !canChangeStructure">mdi-drag</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-row>
<v-col>
<s-text-field
:value="choice.value"
@input="emitInputEnumChoice('updateValue', choiceIdx, $event)"
:disabled="disabled || !canChangeStructure"
:rules="rules.choice"
label="Value"
required
/>
</v-col>
<v-col>
<s-text-field
:value="choice.label"
@input="emitInputEnumChoice('updateLabel', choiceIdx, $event)"
:disabled="disabled"
label="Label"
required
/>
</v-col>
</v-row>
</v-list-item-content>
<v-list-item-action>
<btn-delete :delete="() => emitInputEnumChoice('delete', choiceIdx)" :disabled="disabled || !canChangeStructure" icon />
</v-list-item-action>
</v-list-item>
</draggable>
<v-list-item>
<v-list-item-action>
<s-btn @click="emitInputEnumChoice('add')" :disabled="disabled || !canChangeStructure" color="secondary">
@ -186,6 +198,7 @@
<script>
import { omit } from 'lodash';
import Draggable from 'vuedraggable';
import { uniqueName } from '~/utils/state';
const DATA_TYPES = {
@ -203,6 +216,7 @@ const DATA_TYPES = {
};
export default {
components: { Draggable },
props: {
value: {
type: Object,
@ -233,7 +247,7 @@ export default {
id => (
// this.parentObject.filter(f => id === f.id).length === 1 &&
// TODO: validate ID unique abd validate custom ID not in list of core and predefined field IDs
/^[a-zA-Z_][a-zA-Z0-9_]+$/.test(id)
/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(id)
) || 'Invalid field ID',
],
choice: [
@ -247,7 +261,7 @@ export default {
DATA_TYPES: () => DATA_TYPES,
objectFields() {
if (this.value.type === DATA_TYPES.object) {
return Object.keys(this.value.properties || {}).sort().map(f => ({ id: f, ...this.value.properties[f] }));
return Object.keys(this.value.properties || {}).map(f => ({ id: f, ...this.value.properties[f] }));
} else {
return [];
}
@ -289,22 +303,24 @@ export default {
const newObj = Object.assign({}, this.value, Object.fromEntries([[property, val]]));
this.$emit('input', newObj);
},
emitInputEnumChoice(action, choice, val = null) {
emitInputEnumChoice(action, choiceIdx, val = null) {
const newObj = Object.assign({}, this.value, { choices: [...this.value.choices] });
if (action === 'updateValue') {
newObj.choices.filter(c => c.value === choice.value)[0].value = val;
newObj.choices[choiceIdx].value = val;
} else if (action === 'updateLabel') {
newObj.choices.filter(c => c.value === choice.value)[0].label = val;
newObj.choices[choiceIdx].label = val;
} else if (action === 'delete') {
newObj.choices = newObj.choices.filter(c => c.value !== choice.value);
newObj.choices = newObj.choices.filter((c, idx) => idx !== choiceIdx);
} else if (action === 'add') {
if (val === null) {
val = {
value: uniqueName('new_value', newObj.choices.map(c => c.id)),
value: uniqueName('new_value', newObj.choices.map(c => c.value)),
label: 'New Enum Value',
}
}
newObj.choices.push(val);
} else if (action === 'sort') {
newObj.choices = val;
}
this.$emit('input', newObj);
@ -318,6 +334,7 @@ export default {
} else if (action === 'add') {
newObj.suggestions.push('New Value');
}
// TODO: allow sorting of suggestions
this.$emit('input', newObj);
},
emitInputObject(action, fieldId = null, val = null) {
@ -333,6 +350,7 @@ export default {
type: DATA_TYPES.string,
label: 'New Field',
required: true,
spellcheck: false,
default: null,
};
}
@ -348,3 +366,9 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.draggable-handle {
cursor: grab;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div v-intersect="onIntersect">
<div v-intersect="onIntersect" class="h-100">
<split-menu :value="20">
<template #menu>
<v-list dense>

View File

@ -2,17 +2,17 @@
<draggable
:value="value.children"
draggable=".draggable-item"
handle=".draggable-handle"
filter=".draggable-item-disabled"
:group="{name: 'designerComponents', put: ['designerComponents', 'predefinedDesignerCompnents']}"
@change="onChange"
:delay="50"
:disabled="disabled"
class="pb-1"
>
<div v-for="item in value.children" :key="item.id" class="draggable-item" :class="{'draggable-item-disabled': !item.canMove}">
<v-list-item class="list-item" link :ripple="false">
<v-list-item-icon>
<v-icon>mdi-drag-horizontal</v-icon>
<v-list-item-icon class="draggable-handle">
<v-icon :disabled="disabled">mdi-drag-horizontal</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
@ -125,8 +125,11 @@ export default {
margin-left: 1rem;
}
.list-item {
.draggable-handle {
cursor: grab;
}
.list-item {
min-height: 1em;
& .v-list-item__action {

View File

@ -12,7 +12,7 @@
<v-list-item
v-for="finding in projectType.report_preview_data.findings" :key="finding.id"
:value="finding"
:class="'finding-level-' + riskLevel(finding.cvss)"
:class="'finding-level-' + riskLevel(finding)"
link
>
<v-list-item-title>{{ finding.title }}</v-list-item-title>
@ -118,9 +118,12 @@ export default {
updateFindingField(fieldId, value) {
const newVal = Object.assign({}, this.value);
const newFinding = Object.assign({}, this.currentItem, Object.fromEntries([[fieldId, value]]));
newVal.findings = sortFindings(
this.value.findings
.map(f => f.id === newFinding.id ? newFinding : f));
newVal.findings = sortFindings({
findings: this.value.findings.map(f => f.id === newFinding.id ? newFinding : f),
projectType: this.projectType,
overrideFindingOrder: false,
topLevelFields: true,
});
this.$emit('input', newVal);
this.currentItem = newFinding;
},
@ -152,8 +155,14 @@ export default {
newVal.findings = this.value.findings.filter(f => f.id !== finding.id);
this.$emit('input', newVal);
},
riskLevel(cvssVector) {
return cvss.levelNumberFromScore(cvss.scoreFromVector(cvssVector));
riskLevel(finding) {
if ('severity' in this.projectType.finding_fields) {
return cvss.levelNumberFromLevelName(finding.severity);
} else if ('cvss' in this.projectType.finding_fields) {
return cvss.levelNumberFromScore(cvss.scoreFromVector(finding.cvss));
} else {
return 'unknown';
}
},
}
}

View File

@ -45,6 +45,7 @@
:label="label"
:disabled="disabled"
clearable
class="mt-4"
/>
<s-combobox
v-else-if="definition.type === 'combobox'"

View File

@ -0,0 +1,62 @@
<template>
<div
class="drag-drop-area"
@drop.prevent="onDrop"
@dragover.prevent="showDropArea = true"
@dragenter.prevent="showDropArea = true"
@dragleave.prevent="showDropArea = false"
>
<slot name="default" />
<v-fade-transition v-if="!disabled">
<v-overlay v-if="showDropArea" absolute>
<div class="text-center mt-10">
<h2>Drop files to upload</h2>
</div>
</v-overlay>
</v-fade-transition>
</div>
</template>
<script>
export default {
props: {
disabled: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: false,
},
},
emits: ['drop'],
data() {
return {
showDropArea: false,
};
},
methods: {
onDrop(event) {
this.showDropArea = false;
if (this.disabled) {
return;
}
const files = Array.from(event.dataTransfer.files);
if (!this.multiple && files.length > 1) {
this.$toast.error('Only one file can be uploaded at a time');
return;
}
this.$emit('drop', files);
},
},
}
</script>
<style lang="scss" scoped>
.drag-drop-area {
min-height: 100%;
}
</style>

View File

@ -0,0 +1,11 @@
<template>
<div class="h-100 d-flex flex-column">
<div class="flex-grow-0">
<slot name="header" />
</div>
<div class="flex-grow-height overflow-y-auto">
<slot name="default" />
</div>
</div>
</template>

View File

@ -6,7 +6,6 @@
item-value="code"
:item-text="l => l.name + (l.code ? ' (' + l.code + ')' : '')"
label="Language"
class="mt-4"
>
<template #prepend-inner>
<v-icon small left>mdi-translate</v-icon>

View File

@ -1,13 +1,24 @@
<template>
<v-container>
<h1><slot name="title" /></h1>
<v-container class="pt-0">
<v-list v-if="items" class="pt-0">
<div class="list-header pt-2">
<h1><slot name="title" /></h1>
<slot name="searchbar" :items="items">
<v-text-field :value="items.searchQuery" @input="updateSearch" label="Search" spellcheck="false" autofocus />
</slot>
<slot name="searchbar" :items="items">
<v-text-field
:value="items.searchQuery"
@input="updateSearch"
label="Search"
spellcheck="false"
hide-details="auto"
autofocus
class="mt-0 mb-2"
/>
</slot>
<slot name="actions" />
</div>
<slot name="actions" />
<v-list v-if="items">
<slot v-for="item in items.data" name="item" :item="item" />
<page-loader :items="items" />
<v-list-item v-if="items.data.length === 0 && !items.hasNextPage">
@ -61,3 +72,12 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.list-header {
position: sticky;
top: 0;
z-index: 1;
background-color: white;
}
</style>

View File

@ -5,7 +5,7 @@
</template>
<script>
import { EditorState, EditorSelection } from '@codemirror/state';
import { EditorState } from '@codemirror/state';
import { EditorView, tooltips } from '@codemirror/view';
import { history } from '@codemirror/commands';
import { forceLinting, setDiagnostics } from '@codemirror/lint';
@ -43,10 +43,11 @@ export default {
valueNotNull() {
return this.value || '';
},
spellcheckEnabled() {
return this.lang !== null && !this.disabled && this.spellcheckSupported &&
this.$store.state.settings.spellcheckEnabled && this.$store.getters['apisettings/settings'].features.spellcheck &&
this.$store.getters['apisettings/settings'].languages.find(l => l.code === this.lang)?.spellcheck;
spellcheckLanguageToolEnabled() {
return !this.disabled && this.spellcheckSupported && this.$store.getters['settings/spellcheckLanguageToolEnabled'](this.lang);
},
spellcheckBrowserEnabled() {
return !this.disabled && this.spellcheckSupported && this.$store.getters['settings/spellcheckBrowserEnabled'](this.lang);
},
},
watch: {
@ -67,17 +68,20 @@ export default {
}
},
lang(val) {
if (this.spellcheckEnabled) {
if (this.spellcheckLanguageToolEnabled) {
forceLinting(this.editorView);
}
},
spellcheckEnabled(val) {
this.editorActions.spellcheck(val);
spellcheckLanguageToolEnabled(val) {
this.editorActions.spellcheckLanguageTool(val);
if (!val) {
// clear existing spellcheck items from editor
this.editorView.dispatch(setDiagnostics(this.editorView.state, []));
}
}
},
spellcheckBrowserEnabled(val) {
this.editorActions.spellcheckBrowser(val);
},
},
mounted() {
this.editorView = new EditorView({
@ -125,13 +129,17 @@ export default {
EditorView.editable.of(false),
EditorState.readOnly.of(true),
]),
spellcheck: createEditorExtensionToggler(this.editorView, [
spellcheckLanguageTool: createEditorExtensionToggler(this.editorView, [
spellcheck({ performSpellcheckRequest: this.performSpellcheckRequest, performSpellcheckAddWordRequest: this.performSpellcheckAddWordRequest }),
spellcheckTheme,
]),
spellcheckBrowser: createEditorExtensionToggler(this.editorView, [
EditorView.contentAttributes.of({ spellcheck: true }),
]),
};
this.editorActions.disabled(this.disabled);
this.editorActions.spellcheck(this.spellcheckEnabled);
this.editorActions.spellcheckLanguageTool(this.spellcheckLanguageToolEnabled);
this.editorActions.spellcheckBrowser(this.spellcheckBrowserEnabled);
},
beforeDestroy() {
if (this.editorView) {

View File

@ -15,7 +15,23 @@
<input ref="fileInput" type="file" multiple @change="onUploadFiles" :disabled="disabled || fileUploadInProgress" class="d-none" />
</template>
<span class="separator" />
<markdown-toolbar-button @click="spellcheckEnabled = !spellcheckEnabled" title="Spellcheck" icon="mdi-spellcheck" :disabled="disabled || !spellcheckSupported" :active="spellcheckEnabled" />
<markdown-toolbar-button
v-if="isProfessionalLicense"
@click="spellcheckEnabled = !spellcheckEnabled"
title="Spellcheck"
icon="mdi-spellcheck"
:disabled="disabled || !spellcheckSupported"
:active="spellcheckEnabled"
/>
<markdown-toolbar-button
v-else
@click="toggleBrowserSpellcheckCommunity"
:title="'Spellcheck (browser-based)'"
icon="mdi-spellcheck"
:dot="!spellcheckSupported ? null : (spellcheckEnabled ? 'warning' : 'error')"
:disabled="disabled || !spellcheckSupported"
:active="spellcheckEnabled"
/>
<span class="separator" />
<markdown-toolbar-button @click="undo" title="Undo" icon="mdi-undo" :disabled="disabled || !canUndo" />
<markdown-toolbar-button @click="redo" title="Redo" icon="mdi-redo" :disabled="disabled || !canRedo" />
@ -62,9 +78,18 @@ export default {
this.$store.commit('settings/updateMarkdownEditorMode', val);
},
},
isProfessionalLicense() {
return this.$store.getters['apisettings/isProfessionalLicense'];
},
spellcheckLanguageToolSupported() {
return this.$store.getters['apisettings/spellcheckLanguageToolSupportedForLanguage'](this.lang);
},
spellcheckSupported() {
return this.$store.getters['apisettings/settings'].features.spellcheck &&
this.$store.getters['apisettings/settings'].languages.find(l => l.code === this.lang)?.spellcheck;
if (this.isProfessionalLicense) {
return this.spellcheckLanguageToolSupported;
} else {
return this.lang !== null;
}
},
spellcheckEnabled: {
get() {
@ -126,6 +151,12 @@ export default {
this.$refs.fileInput.value = null;
}
},
toggleBrowserSpellcheckCommunity() {
this.spellcheckEnabled = !this.spellcheckEnabled;
if (this.spellcheckEnabled) {
this.$toast.global.warning({ message: 'Enabled browser-based spellcheck. Upgrade to Professional for built-in spellchecking.' });
}
},
}
}
</script>

View File

@ -2,7 +2,10 @@
<s-tooltip>
<template #activator="{on, attrs}">
<s-btn :disabled="disabled" @click="$emit('click', $event)" v-bind="attrs" v-on="on" icon small tile :outlined="active">
<v-icon small>{{ icon }}</v-icon>
<v-badge v-if="dot" dot overlap :color="dot">
<v-icon small>{{ icon }}</v-icon>
</v-badge>
<v-icon v-else small>{{ icon }}</v-icon>
</s-btn>
</template>
<template #default><span>{{ title }}</span></template>
@ -28,6 +31,10 @@ export default {
type: Boolean,
default: false,
},
dot: {
type: String,
default: null,
},
},
}
</script>

View File

@ -6,7 +6,7 @@
group="notes"
:delay="50"
:disabled="disabled"
class="pb-1 flex-grow-1 overflow-y-auto"
class="pb-1"
>
<div v-for="item in value" :key="item.note.id" class="draggable-item">
<v-list-item

View File

@ -1,5 +1,5 @@
<template>
<v-menu :close-on-content-click="false" max-width="500px" bottom offset-y>
<v-menu :close-on-content-click="false" max-width="500px" max-height="90vh" bottom offset-y>
<template #activator="{attrs: menuAttrs, on: menuOn}">
<s-btn v-bind="menuAttrs" v-on="menuOn" icon dark>
<v-badge v-if="unreadNotificationCount > 0" color="primary" :content="unreadNotificationCount" overlap>

View File

@ -1,26 +1,24 @@
<template>
<div>
<fill-screen-height class="d-flex flex-column">
<pdf :value="pdfData" class="flex-grow-1" />
<div class="h-100 d-flex flex-column pos-relative">
<pdf :value="pdfData" class="flex-grow-height" />
<v-footer padless dark class="footer">
<v-btn @click="showMessages = !showMessages" small width="100%" :ripple="false">
<v-spacer />
<span class="mr-6"><v-icon left small color="error">mdi-close-circle</v-icon> {{ messages.filter(m => m.level === 'error' ).length }}</span>
<span class="mr-6"><v-icon left small color="warning">mdi-alert</v-icon> {{ messages.filter(m => m.level === 'warning' ).length }}</span>
<span><v-icon left small color="info">mdi-message-text</v-icon> {{ messages.filter(m => m.level === 'info' ).length }}</span>
</v-btn>
</v-footer>
<v-footer padless dark class="footer">
<v-btn @click="showMessages = !showMessages" small width="100%" :ripple="false">
<v-spacer />
<span class="mr-6"><v-icon left small color="error">mdi-close-circle</v-icon> {{ messages.filter(m => m.level === 'error' ).length }}</span>
<span class="mr-6"><v-icon left small color="warning">mdi-alert</v-icon> {{ messages.filter(m => m.level === 'warning' ).length }}</span>
<span><v-icon left small color="info">mdi-message-text</v-icon> {{ messages.filter(m => m.level === 'info' ).length }}</span>
</v-btn>
</v-footer>
<v-overlay v-if="showMessages" color="grey darken-4" opacity="0.7" absolute>
<error-list :value="messages" :show-no-message-info="true" class="mt-5" />
</v-overlay>
<v-overlay v-else-if="renderingInProgress && (!pdfData || showLoadingSpinnerOnReload)" absolute z-index="20">
<div class="initial-loading">
<v-progress-circular indeterminate />
</div>
</v-overlay>
</fill-screen-height>
<v-overlay v-if="showMessages" color="grey darken-4" opacity="0.7" absolute>
<error-list :value="messages" :show-no-message-info="true" class="mt-5" />
</v-overlay>
<v-overlay v-else-if="renderingInProgress && (!pdfData || showLoadingSpinnerOnReload)" absolute z-index="20">
<div class="initial-loading">
<v-progress-circular indeterminate />
</div>
</v-overlay>
</div>
</template>
@ -124,4 +122,8 @@ export default {
.footer {
z-index: 10;
}
.pos-relative {
position: relative;
}
</style>

View File

@ -9,6 +9,7 @@
:rules="rules"
:loading="items.isLoading"
:clearable="!required"
class="mt-4"
v-bind="$attrs"
>
<template #append-item>
@ -97,7 +98,7 @@ export default {
this.$emit('input', t);
}
})
.catch(this.$toast.requestError);
.catch(this.$toast.global.requestError);
}
}

View File

@ -6,7 +6,6 @@
:error-count="100"
persistent-hint
:outlined="outlined"
class="mt-4"
spellcheck="false"
>
<template v-for="_, name in $scopedSlots" :slot="name" slot-scope="data"><slot :name="name" v-bind="data" /></template>

View File

@ -1,18 +1,14 @@
<template>
<splitpanes @resized="$emit('input', $event[0].size)" class="default-theme">
<pane :size="value">
<fill-screen-height>
<v-navigation-drawer permanent left width="100%">
<slot name="menu" />
</v-navigation-drawer>
</fill-screen-height>
<splitpanes @resized="$emit('input', $event[0].size)" class="h-100 default-theme">
<pane :size="value" class="h-100 overflow-y-auto">
<v-navigation-drawer permanent left width="100%">
<slot name="menu" />
</v-navigation-drawer>
</pane>
<pane :size="100 - value">
<fill-screen-height>
<v-container fluid class="pt-0 pb-0">
<slot name="default" />
</v-container>
</fill-screen-height>
<pane :size="100 - value" class="h-100 overflow-y-auto">
<v-container fluid class="pt-0 pb-0">
<slot name="default" />
</v-container>
</pane>
</splitpanes>
</template>

View File

@ -72,6 +72,7 @@
@input="v => updateTranslationField(translation, 'language', v)"
:items="[currentLanguageInfo].concat(unusedLanguageInfos)"
:disabled="readonly"
class="mt-4"
/>
<div v-for="d in visibleFieldDefinitionsExceptTitle" :key="d.id" class="d-flex flex-row">
@ -85,7 +86,7 @@
:upload-file="uploadFile"
:rewrite-file-url="rewriteFileUrl"
:disabled="readonly || (!translation.is_main && !(d.id in translation.data))"
class="template-input-field"
class="flex-grow-width"
/>
<div v-if="!translation.is_main && d.id !== 'title'" class="mt-4">
<s-tooltip v-if="d.id in translation.data">
@ -274,12 +275,3 @@ export default {
},
}
</script>
<style lang="scss" scoped>
.template-input-field {
// Fill up remaining space
flex-grow: 1;
// Prevent flex item from overflowing container when input element is too long
min-width: 0;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<v-menu left bottom offset-y>
<v-menu left bottom offset-y max-height="90vh">
<template #activator="{ on, attrs }">
<v-btn v-bind="attrs" v-on="on" icon dark>
<v-badge v-if="licenseError" dot color="error"><v-icon>mdi-account</v-icon></v-badge>
@ -21,7 +21,7 @@
<v-list-item-icon><v-icon>mdi-account</v-icon></v-list-item-icon>
<v-list-item-title>Profile</v-list-item-title>
</v-list-item>
<template v-if="$store.getters['apisettings/is_professional_license']">
<template v-if="$store.getters['apisettings/isProfessionalLicense']">
<v-list-item v-if="$auth.hasScope('admin')" to="/users/self/admin/disable/" nuxt>
<v-list-item-icon><v-icon>mdi-account-arrow-down</v-icon></v-list-item-icon>
<v-list-item-title>Disable Superuser Permissions</v-list-item-title>

View File

@ -1,6 +1,7 @@
<template>
<s-autocomplete
:value="value" @change="$emit('input', $event)"
class="mt-4"
v-bind="autocompleteAttrs"
>
<template v-for="_, name in $scopedSlots" :slot="name" slot-scope="data"><slot :name="name" v-bind="data" /></template>

View File

@ -1,5 +1,5 @@
<template>
<v-app>
<v-app class="height-fullscreen">
<v-app-bar app absolute dense elevation="0">
<v-tabs class="main-menu" hide-slider>
<v-tab to="/" nuxt active-class="no-highlight" exact :ripple="false">
@ -16,7 +16,7 @@
</template>
<v-spacer />
<s-tooltip v-if="$auth.loggedIn && $auth.user.is_superuser && !$auth.hasScope('admin') && $store.getters['apisettings/is_professional_license']" bottom>
<s-tooltip v-if="$auth.loggedIn && $auth.user.is_superuser && !$auth.hasScope('admin') && $store.getters['apisettings/isProfessionalLicense']" bottom>
<template #activator="{ on, attrs }">
<s-btn to="/users/self/admin/enable/" v-bind="attrs" v-on="on" large dark class="btn-sudo">
<v-icon>mdi-account-arrow-up</v-icon>
@ -38,7 +38,7 @@
</v-tabs>
</v-app-bar>
<v-main>
<v-main class="main-container">
<Nuxt />
</v-main>
</v-app>
@ -58,6 +58,19 @@ export default {
</script>
<style lang="scss" scoped>
.height-fullscreen {
height: 100vh;
}
.main-container {
height: 100%;
& > :deep(.v-main__wrap) {
height: 100%;
overflow-y: auto;
}
}
.badge-pill {
margin-bottom: 0.7em;

View File

@ -51,10 +51,11 @@ export default {
editorMode() {
return this.$store.state.settings.markdownEditorMode;
},
spellcheckEnabled() {
return this.lang !== null && !this.disabled &&
this.$store.state.settings.spellcheckEnabled && this.$store.getters['apisettings/settings'].features.spellcheck &&
this.$store.getters['apisettings/settings'].languages.find(l => l.code === this.lang)?.spellcheck;
spellcheckLanguageToolEnabled() {
return !this.disabled && this.$store.getters['settings/spellcheckLanguageToolEnabled'](this.lang);
},
spellcheckBrowserEnabled() {
return !this.disabled && this.$store.getters['settings/spellcheckBrowserEnabled'](this.lang);
},
},
watch: {
@ -75,17 +76,20 @@ export default {
}
},
lang(val) {
if (this.spellcheckEnabled) {
if (this.spellcheckLanguageToolEnabled) {
forceLinting(this.editorView);
}
},
spellcheckEnabled(val) {
this.editorActions.spellcheck(val);
spellcheckLanguageToolEnabled(val) {
this.editorActions.spellcheckLanguageTool(val);
if (!val) {
// clear existing spellcheck items from editor
this.editorView.dispatch(setDiagnostics(this.editorView.state, []));
}
}
},
spellcheckBrowserEnabled(val) {
this.editorActions.spellcheckBrowser(val);
},
},
mounted() {
this.initializeEditorView();
@ -128,10 +132,13 @@ export default {
EditorView.editable.of(false),
EditorState.readOnly.of(true),
]),
spellcheck: createEditorExtensionToggler(this.editorView, [
spellcheckLanguageTool: createEditorExtensionToggler(this.editorView, [
spellcheck({ performSpellcheckRequest: this.performSpellcheckRequest, performSpellcheckAddWordRequest: this.performSpellcheckAddWordRequest }),
spellcheckTheme,
]),
spellcheckBrowser: createEditorExtensionToggler(this.editorView, [
EditorView.contentAttributes.of({ spellcheck: true }),
]),
uploadFile: createEditorExtensionToggler(this.editorView, [
EditorView.domEventHandlers({
drop: (event, view) => {
@ -151,7 +158,8 @@ export default {
]),
};
this.editorActions.disabled(this.disabled);
this.editorActions.spellcheck(this.spellcheckEnabled);
this.editorActions.spellcheckLanguageTool(this.spellcheckLanguageToolEnabled);
this.editorActions.spellcheckBrowser(this.spellcheckBrowserEnabled);
this.editorActions.uploadFile(this.uploadFile !== null);
},
additionalCodeMirrorExtensions() {
@ -189,7 +197,7 @@ export default {
}
},
async performSpellcheckRequest(data) {
if (!this.spellcheckEnabled || !data) {
if (!this.spellcheckLanguageToolEnabled || !data) {
return {
matches: []
};

View File

@ -174,6 +174,14 @@ export default {
type: 'error',
icon: 'mdi-alert-outline'
}
},
{
name: 'warning',
message: ({ message }) => message,
options: {
icon: 'mdi-alert-outline',
className: 'toast-warning',
},
}
]
},

View File

@ -1,12 +1,14 @@
<template>
<div>
<s-sub-menu>
<v-tab :to="`/designs/${$route.params.projectTypeId}/`" nuxt exact>General Settings</v-tab>
<v-tab :to="`/designs/${$route.params.projectTypeId}/pdfdesigner/`" nuxt>PDF Designer</v-tab>
<v-tab :to="`/designs/${$route.params.projectTypeId}/reportfields/`" nuxt>Report Fields</v-tab>
<v-tab :to="`/designs/${$route.params.projectTypeId}/findingfields/`" nuxt>Finding Fields</v-tab>
</s-sub-menu>
<full-height-page>
<template #header>
<s-sub-menu>
<v-tab :to="`/designs/${$route.params.projectTypeId}/`" nuxt exact>General Settings</v-tab>
<v-tab :to="`/designs/${$route.params.projectTypeId}/pdfdesigner/`" nuxt>PDF Designer</v-tab>
<v-tab :to="`/designs/${$route.params.projectTypeId}/reportfields/`" nuxt>Report Fields</v-tab>
<v-tab :to="`/designs/${$route.params.projectTypeId}/findingfields/`" nuxt>Finding Fields</v-tab>
</s-sub-menu>
</template>
<NuxtChild />
</div>
<nuxt-child />
</full-height-page>
</template>

View File

@ -1,54 +1,58 @@
<template>
<v-form ref="form">
<v-form ref="form" class="h-100">
<split-menu v-model="menuSize">
<template #menu>
<v-list dense>
<v-list-item-title class="text-h6 pl-2">{{ projectType.name }}</v-list-item-title>
<v-list dense class="pb-0 h-100 d-flex flex-column">
<div>
<v-list-item-title class="text-h6 pl-2">{{ projectType.name }}</v-list-item-title>
</div>
<v-list-item-group v-model="currentField" mandatory>
<v-list-item :value="null" :ripple="false" link>
<v-list-item-title>All Fields</v-list-item-title>
<div class="flex-grow-height overflow-y-auto">
<v-list-item-group v-model="currentField" mandatory>
<v-list-item :value="null" :ripple="false" link>
<v-list-item-title>All Fields</v-list-item-title>
</v-list-item>
<draggable
v-model="findingFields"
:group="{name: 'findingFields', put: ['predefinedFindingFields']}"
draggable=".draggable-item"
@add="addPredefinedField"
:disabled="readonly"
>
<v-list-item v-for="f in findingFields" :key="f.id" :value="f" class="draggable-item" link :ripple="false">
<v-list-item-title>{{ f.id }}</v-list-item-title>
<v-list-item-action>
<btn-delete v-if="f.origin !== 'core'" :delete="() => deleteField(f.id)" icon x-small :disabled="readonly" />
</v-list-item-action>
</v-list-item>
</draggable>
</v-list-item-group>
<v-divider />
<v-list-group :value="true">
<template #activator>
<v-list-item-title>Predefined Fields</v-list-item-title>
</template>
<draggable
draggable=".draggable-item"
:sort="false"
:group="{name: 'predefinedFindingFields'}"
>
<v-list-item v-for="f in availablePredefinedFields" :key="f.id" class="draggable-item" :ripple="false">
<v-list-item-title>{{ f.id }}</v-list-item-title>
</v-list-item>
</draggable>
</v-list-group>
<v-divider class="mb-1" />
<v-list-item>
<s-btn @click.stop="addField" color="secondary" x-small block :disabled="readonly">
<v-icon left>mdi-plus</v-icon>
Add Custom Field
</s-btn>
</v-list-item>
<draggable
v-model="findingFields"
:group="{name: 'findingFields', put: ['predefinedFindingFields']}"
draggable=".draggable-item"
@add="addPredefinedField"
:disabled="readonly"
>
<v-list-item v-for="f in findingFields" :key="f.id" :value="f" class="draggable-item" link :ripple="false">
<v-list-item-title>{{ f.id }}</v-list-item-title>
<v-list-item-action>
<btn-delete v-if="f.origin !== 'core'" :delete="() => deleteField(f.id)" icon x-small :disabled="readonly" />
</v-list-item-action>
</v-list-item>
</draggable>
</v-list-item-group>
<v-divider />
<v-list-group :value="true">
<template #activator>
<v-list-item-title>Predefined Fields</v-list-item-title>
</template>
<draggable
draggable=".draggable-item"
:sort="false"
:group="{name: 'predefinedFindingFields'}"
>
<v-list-item v-for="f in availablePredefinedFields" :key="f.id" class="draggable-item" :ripple="false">
<v-list-item-title>{{ f.id }}</v-list-item-title>
</v-list-item>
</draggable>
</v-list-group>
<v-divider />
<v-list-item>
<s-btn @click.stop="addField" color="secondary" x-small :disabled="readonly">
<v-icon left>mdi-plus</v-icon>
Add Custom Field
</s-btn>
</v-list-item>
</div>
</v-list>
</template>
@ -56,6 +60,12 @@
<edit-toolbar v-bind="toolbarAttrs" v-on="toolbarEvents" :form="$refs.form" />
<template v-if="currentField === null">
<design-finding-ordering-definition
v-model="projectType.finding_ordering"
:project-type="projectType"
:disabled="readonly"
/>
<design-input-field-definition
v-for="f in findingFields" :key="f.id"
:value="f" @input="updateField(f, $event)"
@ -131,6 +141,11 @@ export default {
} else {
this.projectType.finding_field_order = this.projectType.finding_field_order.filter(f => f !== field.id).concat([val.id]);
}
// Remove from finding ordering if data type changed to an unsupported type
if (['list', 'object', 'user'].includes(val.type)) {
this.projectType.finding_ordering = this.projectType.finding_ordering.filter(f => f.field !== val.id);
}
// Update field definition
delete this.projectType.finding_fields[field.id];
@ -159,17 +174,17 @@ export default {
deleteField(fieldId) {
delete this.projectType.finding_fields[fieldId];
this.projectType.finding_field_order = this.projectType.finding_field_order.filter(f => f !== fieldId);
this.projectType.finding_ordering = this.projectType.finding_ordering.filter(f => f.field !== fieldId);
},
async performSave(data) {
await this.$store.dispatch('projecttypes/partialUpdate', { obj: data, fields: ['finding_fields', 'finding_field_order'] });
}
await this.$store.dispatch('projecttypes/partialUpdate', { obj: data, fields: ['finding_fields', 'finding_field_order', 'finding_ordering'] });
},
}
}
</script>
<style lang="scss" scoped>
.draggable-item {
cursor: move;
cursor: grab;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<v-container>
<v-container class="pt-0">
<v-form ref="form">
<edit-toolbar v-bind="toolbarAttrs" v-on="toolbarEvents" :form="$refs.form">
<template #context-menu>
@ -29,7 +29,7 @@
:disabled="readonly"
class="mt-4"
/>
<language-selection v-model="projectType.language" :disabled="readonly" />
<language-selection v-model="projectType.language" :disabled="readonly" class="mt-4" />
</v-form>
</v-container>
</template>

View File

@ -1,48 +1,51 @@
<template>
<div>
<splitpanes class="default-theme">
<pane :size="previewSplitSize">
<edit-toolbar v-bind="toolbarAttrs" v-on="toolbarEvents">
<template #title>{{ projectType.name }}</template>
<div class="h-100">
<splitpanes class="h-100 default-theme">
<pane :size="previewSplitSize" class="h-100">
<full-height-page>
<template #header>
<edit-toolbar v-bind="toolbarAttrs" v-on="toolbarEvents">
<template #title>{{ projectType.name }}</template>
<template #default>
<s-btn
:loading="pdfRenderingInProgress"
:disabled="pdfRenderingInProgress"
@click="loadPdf"
color="secondary"
>
<v-icon>mdi-cached</v-icon>
Refresh PDF
<template #default>
<s-btn
:loading="pdfRenderingInProgress"
:disabled="pdfRenderingInProgress"
@click="loadPdf"
color="secondary"
>
<v-icon>mdi-cached</v-icon>
Refresh PDF
<template #loader>
<saving-loader-spinner />
Refresh PDF
<template #loader>
<saving-loader-spinner />
Refresh PDF
</template>
</s-btn>
</template>
</s-btn>
</template>
</edit-toolbar>
</edit-toolbar>
<v-tabs v-model="currentTab" grow>
<v-tab :value="0">Layout <v-icon right>mdi-flask</v-icon></v-tab>
<v-tab :value="1">HTML+Vue</v-tab>
<v-tab :value="2">CSS</v-tab>
<v-tab :value="3">Assets</v-tab>
<v-tab :value="4">Preview Data</v-tab>
</v-tabs>
<v-tabs-items v-model="currentTab">
<v-tab-item :value="0">
<design-layout-editor
:project-type="projectType"
:upload-file="uploadFile"
:rewrite-file-url="rewriteFileUrl"
:disabled="readonly"
@update="onUpdateCode"
@jump-to-code="jumpToCode"
/>
</v-tab-item>
<v-tab-item :value="1">
<fill-screen-height>
<v-tabs v-model="currentTab" grow>
<v-tab :value="0">Layout <v-icon right>mdi-flask</v-icon></v-tab>
<v-tab :value="1">HTML+Vue</v-tab>
<v-tab :value="2">CSS</v-tab>
<v-tab :value="3">Assets</v-tab>
<v-tab :value="4">Preview Data</v-tab>
</v-tabs>
</template>
<v-tabs-items v-model="currentTab" class="h-100">
<v-tab-item :value="0" class="h-100">
<design-layout-editor
:project-type="projectType"
:upload-file="uploadFile"
:rewrite-file-url="rewriteFileUrl"
:disabled="readonly"
@update="onUpdateCode"
@jump-to-code="jumpToCode"
/>
</v-tab-item>
<v-tab-item :value="1" class="h-100">
<design-code-editor
ref="htmlEditor"
v-model="projectType.report_template"
@ -50,10 +53,8 @@
class="pdf-code-editor"
:disabled="readonly"
/>
</fill-screen-height>
</v-tab-item>
<v-tab-item :value="2">
<fill-screen-height>
</v-tab-item>
<v-tab-item :value="2" class="h-100">
<design-code-editor
ref="cssEditor"
v-model="projectType.report_styles"
@ -61,26 +62,24 @@
class="pdf-code-editor"
:disabled="readonly"
/>
</fill-screen-height>
</v-tab-item>
<v-tab-item :value="3">
<fill-screen-height>
</v-tab-item>
<v-tab-item :value="3" class="h-100 overflow-y-auto">
<design-asset-manager :project-type="projectType" :disabled="readonly" />
</fill-screen-height>
</v-tab-item>
<v-tab-item :value="4">
<design-preview-data-form
v-model="projectType.report_preview_data"
:project-type="projectType"
:upload-file="uploadFile"
:rewrite-file-url="rewriteFileUrl"
:disabled="readonly"
/>
</v-tab-item>
</v-tabs-items>
</v-tab-item>
<v-tab-item :value="4" class="h-100">
<design-preview-data-form
v-model="projectType.report_preview_data"
:project-type="projectType"
:upload-file="uploadFile"
:rewrite-file-url="rewriteFileUrl"
:disabled="readonly"
/>
</v-tab-item>
</v-tabs-items>
</full-height-page>
</pane>
<pane :size="100 - previewSplitSize">
<pane :size="100 - previewSplitSize" class="h-100">
<!-- PDF preview -->
<pdf-preview ref="pdfpreview" :fetch-pdf="fetchPdf" @renderprogress="pdfRenderingInProgress = $event" />
</pane>

View File

@ -1,58 +1,66 @@
<template>
<v-form ref="form">
<v-form ref="form" class="h-100">
<split-menu v-model="menuSize">
<template #menu>
<v-list>
<v-list-item-title class="text-h6 pl-2">{{ projectType.name }}</v-list-item-title>
<v-list class="pb-0 h-100 d-flex flex-column">
<div>
<v-list-item-title class="text-h6 pl-2">{{ projectType.name }}</v-list-item-title>
</div>
<v-list-item-group v-model="currentItem" mandatory>
<draggable
:value="reportSections"
@input="updateSectionOrder"
group="sections"
draggable=".draggable-section"
:disabled="readonly"
>
<div v-for="s in reportSections" :key="s.id" class="draggable-section">
<v-list-item :value="s" :ripple="false" link>
<v-list-item-title>{{ s.label }}</v-list-item-title>
<v-list-item-action>
<btn-delete v-if="s.fields.length === 0" :delete="() => deleteSection(s)" :disabled="readonly" icon small />
</v-list-item-action>
</v-list-item>
<v-list class="sublist" dense>
<draggable
:value="s.fields" @input="updateFieldOrder(s, $event)"
group="fields"
draggable=".draggable-field"
:disabled="readonly"
>
<v-list-item v-for="f in s.fields" :key="f.id" :value="f" class="draggable-field" :ripple="false" link>
<v-list-item-title>{{ f.id }}</v-list-item-title>
<v-list-item-action>
<btn-delete v-if="f.origin !== 'core'" :delete="() => deleteField(s, f)" :disabled="readonly" icon x-small />
</v-list-item-action>
</v-list-item>
</draggable>
<v-list-item>
<s-btn @click.stop="addField(s)" color="secondary" :disabled="readonly" x-small>
<v-icon left>mdi-plus</v-icon>
Add Field
</s-btn>
<div class="flex-grow-height overflow-y-auto">
<v-list-item-group v-model="currentItem" mandatory>
<draggable
:value="reportSections"
@input="updateSectionOrder"
group="sections"
draggable=".draggable-section"
:disabled="readonly"
>
<div v-for="s in reportSections" :key="s.id" class="draggable-section">
<v-list-item :value="s" :ripple="false" link>
<v-list-item-title>{{ s.label }}</v-list-item-title>
<v-list-item-action>
<btn-delete v-if="s.fields.length === 0" :delete="() => deleteSection(s)" :disabled="readonly" icon small />
</v-list-item-action>
</v-list-item>
</v-list>
<v-list class="sublist" dense>
<draggable
:value="s.fields" @input="updateFieldOrder(s, $event)"
group="fields"
draggable=".draggable-field"
:disabled="readonly"
>
<v-list-item v-for="f in s.fields" :key="f.id" :value="f" class="draggable-field" :ripple="false" link>
<v-list-item-title>{{ f.id }}</v-list-item-title>
<v-list-item-action>
<btn-delete v-if="f.origin !== 'core'" :delete="() => deleteField(s, f)" :disabled="readonly" icon x-small />
</v-list-item-action>
</v-list-item>
</draggable>
<v-divider />
</div>
</draggable>
<v-list-item>
<s-btn @click.stop="addField(s)" color="secondary" :disabled="readonly" x-small>
<v-icon left>mdi-plus</v-icon>
Add Field
</s-btn>
</v-list-item>
</v-list>
<v-divider />
</div>
</draggable>
</v-list-item-group>
</div>
<div>
<v-divider class="mb-1" />
<v-list-item>
<s-btn @click.stop="addSection" :disabled="readonly" color="secondary" small>
<s-btn @click.stop="addSection" :disabled="readonly" color="secondary" small block>
<v-icon left>mdi-plus</v-icon>
Add Section
</s-btn>
</v-list-item>
</v-list-item-group>
</div>
</v-list>
</template>
@ -238,7 +246,6 @@ export default {
}
.draggable-field, .draggable-section > .v-list-item {
cursor: move;
cursor: grab;
}
</style>

View File

@ -1,25 +1,29 @@
<template>
<div>
<s-sub-menu v-if="privateDesignsEnabled">
<v-tab :to="`/designs/`" nuxt exact>Global Designs</v-tab>
<v-tab :to="`/designs/private/`" nuxt>Private Designs</v-tab>
</s-sub-menu>
<file-drop-area @drop="$refs.importBtn.performImport($event)" class="h-100">
<full-height-page>
<template #header>
<s-sub-menu v-if="privateDesignsEnabled">
<v-tab :to="`/designs/`" nuxt exact>Global Designs</v-tab>
<v-tab :to="`/designs/private/`" nuxt>Private Designs</v-tab>
</s-sub-menu>
</template>
<list-view url="/projecttypes/?scope=global&ordering=name">
<template #title>Global Designs</template>
<template #actions v-if="$auth.hasScope('designer')">
<design-create-design-dialog project-type-scope="global" />
<btn-import :import="performImport" />
</template>
<template #item="{item}">
<v-list-item :to="`/designs/${item.id}/pdfdesigner/`" nuxt>
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
</v-list-item>
</template>
</list-view>
</div>
<list-view url="/projecttypes/?scope=global&ordering=name">
<template #title>Global Designs</template>
<template #actions v-if="$auth.hasScope('designer')">
<design-create-design-dialog project-type-scope="global" />
<btn-import ref="importBtn" :import="performImport" />
</template>
<template #item="{item}">
<v-list-item :to="`/designs/${item.id}/pdfdesigner/`" nuxt>
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
</v-list-item>
</template>
</list-view>
</full-height-page>
</file-drop-area>
</template>
<script>

View File

@ -1,25 +1,29 @@
<template>
<div>
<s-sub-menu>
<v-tab :to="`/designs/`" nuxt exact>Global Designs</v-tab>
<v-tab :to="`/designs/private/`" nuxt>Private Designs</v-tab>
</s-sub-menu>
<file-drop-area @drop="$refs.importBtn.performImport($event)" class="h-100">
<full-height-page>
<template #header>
<s-sub-menu>
<v-tab :to="`/designs/`" nuxt exact>Global Designs</v-tab>
<v-tab :to="`/designs/private/`" nuxt>Private Designs</v-tab>
</s-sub-menu>
</template>
<list-view url="/projecttypes/?scope=private&ordering=name">
<template #title>Private Designs</template>
<template #actions>
<design-create-design-dialog project-type-scope="private" />
<btn-import :import="performImport" />
</template>
<template #item="{item}">
<v-list-item :to="`/designs/${item.id}/pdfdesigner/`" nuxt>
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
</v-list-item>
</template>
</list-view>
</div>
<list-view url="/projecttypes/?scope=private&ordering=name">
<template #title>Private Designs</template>
<template #actions>
<design-create-design-dialog project-type-scope="private" />
<btn-import ref="importBtn" :import="performImport" />
</template>
<template #item="{item}">
<v-list-item :to="`/designs/${item.id}/pdfdesigner/`" nuxt>
<v-list-item-title>
{{ item.name }}
</v-list-item-title>
</v-list-item>
</template>
</list-view>
</full-height-page>
</file-drop-area>
</template>
<script>

View File

@ -11,6 +11,7 @@
@input="updateNoteOrder"
@update:note="updateNote"
to-prefix="/notes/personal/"
class="flex-grow-1 overflow-y-auto"
/>
<div>
@ -21,7 +22,8 @@
:confirm="false"
button-text="Add"
button-icon="mdi-plus"
tooltip-text="Add Notebook Page"
tooltip-text="Add Note (Ctrl+J)"
keyboard-shortcut="ctrl+j"
color="secondary"
small
block

View File

@ -1,15 +1,17 @@
<template>
<div :key="project.id">
<s-sub-menu>
<v-tab :to="`/projects/${$route.params.projectId}/`" nuxt exact>Project</v-tab>
<v-tab :to="`/projects/${$route.params.projectId}/notes/`" nuxt>Notes</v-tab>
<v-tab :to="`/projects/${$route.params.projectId}/reporting/`" nuxt>Reporting</v-tab>
<v-tab :to="`/projects/${$route.params.projectId}/publish/`" nuxt>Publish</v-tab>
<v-tab :to="`/projects/${$route.params.projectId}/designer/`" nuxt v-if="projectType.source === 'customized'">Designer</v-tab>
</s-sub-menu>
<full-height-page>
<template #header>
<s-sub-menu>
<v-tab :to="`/projects/${$route.params.projectId}/`" nuxt exact>Project</v-tab>
<v-tab :to="`/projects/${$route.params.projectId}/notes/`" nuxt>Notes</v-tab>
<v-tab :to="`/projects/${$route.params.projectId}/reporting/`" nuxt>Reporting</v-tab>
<v-tab :to="`/projects/${$route.params.projectId}/publish/`" nuxt>Publish</v-tab>
<v-tab :to="`/projects/${$route.params.projectId}/designer/`" nuxt v-if="projectType.source === 'customized'">Designer</v-tab>
</s-sub-menu>
</template>
<nuxt-child />
</div>
</full-height-page>
</template>
<script>

View File

@ -1,5 +1,5 @@
<template>
<v-container>
<v-container class="pt-0">
<h1>Archive Project</h1>
<p class="text-h6">
<strong>Name:</strong> {{ project.name }}

View File

@ -1,63 +1,62 @@
<template>
<div>
<splitpanes class="default-theme">
<pane :size="previewSplitSize">
<edit-toolbar v-bind="toolbarAttrs" v-on="toolbarEvents">
<template #title>{{ project.name }}</template>
<div class="h-100">
<splitpanes class="h-100 default-theme">
<pane :size="previewSplitSize" class="h-100">
<full-height-page>
<template #header>
<edit-toolbar v-bind="toolbarAttrs" v-on="toolbarEvents">
<template #title>{{ project.name }}</template>
<template #default>
<s-btn
:loading="pdfRenderingInProgress"
:disabled="pdfRenderingInProgress"
@click="loadPdf"
color="secondary"
>
<v-icon>mdi-cached</v-icon>
Refresh PDF
<template #default>
<s-btn
:loading="pdfRenderingInProgress"
:disabled="pdfRenderingInProgress"
@click="loadPdf"
color="secondary"
>
<v-icon>mdi-cached</v-icon>
Refresh PDF
<template #loader>
<saving-loader-spinner />
Refresh PDF
<template #loader>
<saving-loader-spinner />
Refresh PDF
</template>
</s-btn>
</template>
</s-btn>
</template>
</edit-toolbar>
</edit-toolbar>
<v-tabs grow>
<v-tab>HTML+Vue</v-tab>
<v-tab-item>
<fill-screen-height>
<v-tabs v-model="currentTab" grow>
<v-tab :value="0">HTML+Vue</v-tab>
<v-tab :value="1">CSS</v-tab>
<v-tab :value="2">Assets</v-tab>
</v-tabs>
</template>
<v-tabs-items v-model="currentTab" class="h-100">
<v-tab-item :value="0" class="h-100">
<design-code-editor
v-model="projectType.report_template"
language="html"
class="pdf-code-editor"
:disabled="readonly"
/>
</fill-screen-height>
</v-tab-item>
<v-tab>CSS</v-tab>
<v-tab-item>
<fill-screen-height>
</v-tab-item>
<v-tab-item :value="1" class="h-100">
<design-code-editor
v-model="projectType.report_styles"
language="css"
class="pdf-code-editor"
:disabled="readonly"
/>
</fill-screen-height>
</v-tab-item>
<v-tab>Assets</v-tab>
<v-tab-item>
<fill-screen-height>
</v-tab-item>
<v-tab-item :value="2" class="h-100 overflow-y-auto">
<design-asset-manager :project-type="projectType" :disabled="readonly" />
</fill-screen-height>
</v-tab-item>
</v-tabs>
</v-tab-item>
</v-tabs-items>
</full-height-page>
</pane>
<pane :size="100 - previewSplitSize">
<pane :size="100 - previewSplitSize" class="h-100">
<!-- PDF preview -->
<pdf-preview ref="pdfpreview" :fetch-pdf="fetchPdf" @renderprogress="pdfRenderingInProgress = $event" />
</pane>
@ -80,6 +79,7 @@ export default {
},
data() {
return {
currentTab: 0,
previewSplitSize: 60,
pdfRenderingInProgress: false,
}

View File

@ -1,5 +1,5 @@
<template>
<v-container>
<v-container class="pt-0">
<v-form ref="form">
<edit-toolbar v-bind="toolbarAttrs">
<template #title>Project</template>
@ -80,7 +80,7 @@
</template>
</template>
</project-type-selection>
<language-selection v-model="project.language" :error-messages="serverErrors?.language" :disabled="project.readonly" />
<language-selection v-model="project.language" :error-messages="serverErrors?.language" :disabled="project.readonly" class="mt-4" />
<s-tags
v-model="project.tags"

View File

@ -12,18 +12,20 @@
@update:note="updateNote"
:disabled="project.readonly"
:to-prefix="`/projects/${$route.params.projectId}/notes/`"
class="flex-grow-1 overflow-y-auto"
/>
<div>
<v-divider />
<v-list-item class="mt-1">
<v-divider class="mb-1" />
<v-list-item>
<btn-confirm
:action="createNote"
:disabled="project.readonly"
:confirm="false"
button-text="Add"
button-icon="mdi-plus"
tooltip-text="Add Note"
tooltip-text="Add Note (Ctrl+J)"
keyboard-shortcut="ctrl+j"
small
block
/>

View File

@ -59,6 +59,7 @@
<script>
import urlJoin from 'url-join';
import { omit } from 'lodash';
import { uploadFileHelper } from '~/utils/upload';
import ProjectLockEditMixin from '~/mixins/ProjectLockEditMixin';
@ -110,7 +111,7 @@ export default {
this.$router.push(`/projects/${this.project.id}/notes/`);
},
updateInStore(data) {
this.$store.commit('projects/setNote', { projectId: this.project.id, note: data });
this.$store.commit('projects/setNote', { projectId: this.project.id, note: omit(data, ['parent', 'order']) });
},
async onUpdateData({ oldValue, newValue }) {
const toolbar = this.getToolbarRef();

View File

@ -1,110 +1,106 @@
<template>
<div>
<splitpanes class="default-theme">
<pane :size="previewSplitSize">
<pdf-preview
ref="pdfpreview"
:fetch-pdf="fetchPreviewPdf"
:show-loading-spinner-on-reload="true"
@renderprogress="pdfPreviewInProgress = $event"
/>
</pane>
<splitpanes class="default-theme h-100">
<pane :size="previewSplitSize" class="h-100">
<pdf-preview
ref="pdfpreview"
:fetch-pdf="fetchPreviewPdf"
:show-loading-spinner-on-reload="true"
@renderprogress="pdfPreviewInProgress = $event"
/>
</pane>
<pane :size="100 - previewSplitSize">
<fill-screen-height>
<v-container>
<h1>{{ project.name }}</h1>
<pane :size="100 - previewSplitSize" class="h-100 overflow-y-auto">
<v-container>
<h1>{{ project.name }}</h1>
<v-form class="pa-4">
<!-- Action buttons -->
<div>
<s-btn
:loading="checksOrPreviewInProgress"
:disabled="checksOrPreviewInProgress"
@click="refreshPreviewAndChecks"
color="secondary"
>
<v-icon>mdi-cached</v-icon>
Refresh PDF
<v-form class="pa-4">
<!-- Action buttons -->
<div>
<s-btn
:loading="checksOrPreviewInProgress"
:disabled="checksOrPreviewInProgress"
@click="refreshPreviewAndChecks"
color="secondary"
>
<v-icon>mdi-cached</v-icon>
Refresh PDF
<template #loader>
<saving-loader-spinner />
Refresh PDF
</template>
</s-btn>
<btn-confirm
:action="customizeDesign"
button-text="Customize Design"
button-icon="mdi-file-cog"
tooltip-text="Customize Design for this project"
dialog-text="Customize the current Design for this project. This allows you to adapt the appearence (HTML, CSS) of the design for this project only. The original design is not affected. Any changes made to the original design will not be automatically applied to the adapted design."
:disabled="project.readonly || projectType.source === 'customized'"
/>
</div>
<!-- Set password for encrypting report -->
<div>
<s-checkbox v-model="form.encryptReport" label="Encrypt report PDF" />
<s-text-field
v-if="form.encryptReport"
v-model="form.password"
:error-messages="(form.encryptReport && form.password.length === 0) ? ['Password required'] : []"
label="PDF password"
append-icon="mdi-lock-reset" @click:append="form.password = generateNewPassword()"
class="mt-4"
/>
</div>
<!-- Filename -->
<div>
<s-text-field
v-model="form.filename"
label="Filename"
:rules="rules.filename"
class="mt-4"
/>
</div>
<div class="mt-4">
<btn-confirm
:disabled="!canGenerateFinalReport"
:action="generateFinalReport"
:confirm="false"
button-text="Download"
button-icon="mdi-download"
button-color="primary"
/>
</div>
<div class="mt-4">
<btn-readonly
v-if="!project.readonly"
:value="project.readonly"
:set-readonly="setReadonly"
:disabled="!canGenerateFinalReport"
/>
</div>
</v-form>
<error-list :value="allMessages" :group="true" :show-no-message-info="true">
<template #location="{msg}">
<NuxtLink v-if="messageLocationUrl(msg)" :to="messageLocationUrl(msg)" target="_blank">
in {{ msg.location.type }}
<template v-if="msg.location.name">"{{ msg.location.name }}"</template>
<template v-if="msg.location.path">field "{{ msg.location.path }}"</template>
</NuxtLink>
<span v-else-if="msg.location.name">
in {{ msg.location.type }}
<template v-if="msg.location.name">"{{ msg.location.name }}"</template>
<template v-if="msg.location.path">field "{{ msg.location.path }}"</template>
</span>
<template #loader>
<saving-loader-spinner />
Refresh PDF
</template>
</error-list>
</v-container>
</fill-screen-height>
</pane>
</splitpanes>
</div>
</s-btn>
<btn-confirm
:action="customizeDesign"
button-text="Customize Design"
button-icon="mdi-file-cog"
tooltip-text="Customize Design for this project"
dialog-text="Customize the current Design for this project. This allows you to adapt the appearence (HTML, CSS) of the design for this project only. The original design is not affected. Any changes made to the original design will not be automatically applied to the adapted design."
:disabled="project.readonly || projectType.source === 'customized'"
/>
</div>
<!-- Set password for encrypting report -->
<div>
<s-checkbox v-model="form.encryptReport" label="Encrypt report PDF" />
<s-text-field
v-if="form.encryptReport"
v-model="form.password"
:error-messages="(form.encryptReport && form.password.length === 0) ? ['Password required'] : []"
label="PDF password"
append-icon="mdi-lock-reset" @click:append="form.password = generateNewPassword()"
class="mt-4"
/>
</div>
<!-- Filename -->
<div>
<s-text-field
v-model="form.filename"
label="Filename"
:rules="rules.filename"
class="mt-4"
/>
</div>
<div class="mt-4">
<btn-confirm
:disabled="!canGenerateFinalReport"
:action="generateFinalReport"
:confirm="false"
button-text="Download"
button-icon="mdi-download"
button-color="primary"
/>
</div>
<div class="mt-4">
<btn-readonly
v-if="!project.readonly"
:value="project.readonly"
:set-readonly="setReadonly"
:disabled="!canGenerateFinalReport"
/>
</div>
</v-form>
<error-list :value="allMessages" :group="true" :show-no-message-info="true">
<template #location="{msg}">
<NuxtLink v-if="messageLocationUrl(msg)" :to="messageLocationUrl(msg)" target="_blank">
in {{ msg.location.type }}
<template v-if="msg.location.name">"{{ msg.location.name }}"</template>
<template v-if="msg.location.path">field "{{ msg.location.path }}"</template>
</NuxtLink>
<span v-else-if="msg.location.name">
in {{ msg.location.type }}
<template v-if="msg.location.name">"{{ msg.location.name }}"</template>
<template v-if="msg.location.path">field "{{ msg.location.path }}"</template>
</span>
</template>
</error-list>
</v-container>
</pane>
</splitpanes>
</template>
<script>

View File

@ -1,40 +1,74 @@
<template>
<div>
<split-menu v-model="menuSize">
<template #menu>
<v-list dense class="pb-0 h-100 d-flex flex-column">
<div>
<v-list-item-title class="text-h6 pl-2">{{ project.name }}</v-list-item-title>
</div>
<split-menu v-model="menuSize">
<template #menu>
<v-list dense class="pb-0 h-100 d-flex flex-column">
<div>
<v-list-item-title class="text-h6 pl-2">{{ project.name }}</v-list-item-title>
</div>
<div class="flex-grow-1 overflow-y-auto">
<v-subheader>Sections</v-subheader>
<v-list-item
v-for="section in sections"
:key="section.id"
:to="`/projects/${$route.params.projectId}/reporting/sections/${section.id}/`"
nuxt
>
<lock-info :value="section.lock_info" />
<v-list-item-content>
<v-list-item-title>{{ section.label }}</v-list-item-title>
<v-list-item-subtitle>
<span v-if="section.assignee" :class="{'assignee-self': section.assignee.id == $auth.user.id}">
@{{ section.assignee.username }}
</span>
</v-list-item-subtitle>
</v-list-item-content>
<status-info :value="section.status" />
</v-list-item>
<div class="flex-grow-1 overflow-y-auto">
<v-subheader>Sections</v-subheader>
<v-list-item
v-for="section in sections"
:key="section.id"
:to="`/projects/${$route.params.projectId}/reporting/sections/${section.id}/`"
nuxt
>
<lock-info :value="section.lock_info" />
<v-list-item-content>
<v-list-item-title>{{ section.label }}</v-list-item-title>
<v-list-item-subtitle>
<span v-if="section.assignee" :class="{'assignee-self': section.assignee.id == $auth.user.id}">
@{{ section.assignee.username }}
</span>
</v-list-item-subtitle>
</v-list-item-content>
<status-info :value="section.status" />
</v-list-item>
<v-subheader>Findings</v-subheader>
<v-subheader>
Findings
<v-spacer />
<s-tooltip>
<template #activator="{on}">
<s-btn
@click="toggleOverrideFindingOrder"
:disabled="project.readonly"
small
icon
v-on="on"
>
<v-icon v-if="project.override_finding_order" small>mdi-sort-variant-off</v-icon>
<v-icon v-else small>mdi-sort-variant</v-icon>
</s-btn>
</template>
<template #default>
<span v-if="project.override_finding_order">Custom order</span>
<span v-else>Default order</span>
</template>
</s-tooltip>
</v-subheader>
<draggable
:value="findings"
@input="sortFindings"
draggable=".draggable-item"
handle=".draggable-handle"
:disabled="project.readonly || !project.override_finding_order"
>
<v-list-item
v-for="finding in findings"
:key="finding.id"
:to="`/projects/${$route.params.projectId}/reporting/findings/${finding.id}/`"
nuxt
:class="'finding-level-' + riskLevel(finding.data.cvss)"
:ripple="false"
class="draggable-item"
:class="'finding-level-' + riskLevel(finding)"
>
<v-list-item-icon v-if="project.override_finding_order" class="draggable-handle mr-2">
<v-icon :disabled="disabled">mdi-drag-horizontal</v-icon>
</v-list-item-icon>
<lock-info :value="finding.lock_info" />
<v-list-item-content>
<v-list-item-title>{{ finding.data.title }}</v-list-item-title>
@ -46,38 +80,41 @@
</v-list-item-content>
<status-info :value="finding.status" />
</v-list-item>
</div>
</draggable>
</div>
<div>
<v-divider />
<v-list-item class="mt-1">
<create-finding-dialog :project="project" />
</v-list-item>
</div>
</v-list>
</template>
<div>
<v-divider class="mb-1" />
<v-list-item>
<create-finding-dialog :project="project" />
</v-list-item>
</div>
</v-list>
</template>
<template #default>
<NuxtChild />
</template>
</split-menu>
</div>
<template #default>
<NuxtChild />
</template>
</split-menu>
</template>
<script>
import Draggable from 'vuedraggable';
import * as cvss from '@/utils/cvss.js';
export default {
components: { Draggable },
async asyncData({ params, store }) {
const project = store.dispatch('projects/getById', params.projectId);
const findings = store.dispatch('projects/getFindings', params.projectId);
const sections = store.dispatch('projects/getSections', params.projectId);
await Promise.all([project, findings, sections]);
return { project: await project };
const project = await store.dispatch('projects/fetchById', params.projectId);
return {
project: await project,
projectType: await store.dispatch('projecttypes/getById', project.project_type),
};
},
data() {
return {
refreshListingsInterval: null,
wasOverrideFindingOrder: false,
}
},
head: {
@ -85,7 +122,7 @@ export default {
},
computed: {
findings() {
return this.$store.getters['projects/findings'](this.project.id);
return this.$store.getters['projects/findings'](this.project.id, { projectType: this.projectType });
},
sections() {
return this.$store.getters['projects/sections'](this.project.id);
@ -99,6 +136,14 @@ export default {
}
},
},
watch: {
'project.override_finding_order': {
immediate: true,
handler() {
this.wasOverrideFindingOrder ||= this.project.override_finding_order;
}
}
},
mounted() {
this.refreshListingsInterval = setInterval(this.refreshListings, 10_000);
},
@ -109,17 +154,43 @@ export default {
}
},
methods: {
refreshListings() {
async refreshListings() {
try {
this.$store.dispatch('projects/fetchFindings', this.project.id);
this.$store.dispatch('projects/fetchSections', this.project.id);
this.project = await this.$store.dispatch('projects/fetchById', this.project.id);
this.project_type = await this.$store.dispatch('projecttypes/getById', this.project.project_type);
} catch (error) {
// hide error
}
},
riskLevel(cvssVector) {
return cvss.levelNumberFromScore(cvss.scoreFromVector(cvssVector));
}
riskLevel(finding) {
if ('severity' in this.projectType.finding_fields) {
return cvss.levelNumberFromLevelName(finding.data.severity);
} else if ('cvss' in this.projectType.finding_fields) {
return cvss.levelNumberFromScore(cvss.scoreFromVector(finding.data.cvss));
} else {
return 'unknown';
}
},
async toggleOverrideFindingOrder() {
if (!this.wasOverrideFindingOrder) {
// Use current sort order as starting point
// But prevent destorying previous overwritten order on toggle
await this.sortFindings(this.findings);
}
this.project = await this.$store.dispatch('projects/partialUpdate', {
obj: {
id: this.project.id,
override_finding_order: !this.project.override_finding_order,
}
});
},
async sortFindings(findings) {
await this.$store.dispatch('projects/sortFindings', {
projectId: this.project.id,
findings,
});
},
}
};
</script>
@ -134,4 +205,8 @@ export default {
:deep(.v-list-item__subtitle) {
font-size: x-small !important;
}
.draggable-handle {
cursor: grab;
}
</style>

View File

@ -51,6 +51,7 @@
<script>
import urlJoin from 'url-join';
import { omit } from 'lodash';
import ProjectLockEditMixin from '~/mixins/ProjectLockEditMixin.js';
export default {
@ -89,7 +90,7 @@ export default {
this.$router.push(`/projects/${data.project}/reporting/`);
},
updateInStore(data) {
this.$store.commit('projects/setFinding', { projectId: data.project, finding: data });
this.$store.commit('projects/setFinding', { projectId: data.project, finding: omit(data, ['order']) });
},
async onUpdateData({ oldValue, newValue }) {
const toolbar = this.getToolbarRef();

View File

@ -1,5 +1,5 @@
<template>
<v-container>
<v-container class="pt-0">
<edit-toolbar>
<template #title>{{ archive.name }}</template>

View File

@ -1,10 +1,12 @@
<template>
<div>
<s-sub-menu>
<v-tab to="/projects/" nuxt exact>Active Projects</v-tab>
<v-tab to="/projects/finished/" nuxt>Finished Projects</v-tab>
<v-tab to="/projects/archived/" nuxt>Archived Projects</v-tab>
</s-sub-menu>
<full-height-page>
<template #header>
<s-sub-menu>
<v-tab to="/projects/" nuxt exact>Active Projects</v-tab>
<v-tab to="/projects/finished/" nuxt>Finished Projects</v-tab>
<v-tab to="/projects/archived/" nuxt>Archived Projects</v-tab>
</s-sub-menu>
</template>
<list-view url="/archivedprojects/">
<template #title>Archived Projects</template>
@ -50,7 +52,7 @@
</v-list-item>
</template>
</list-view>
</div>
</full-height-page>
</template>
<script>

View File

@ -1,10 +1,12 @@
<template>
<div>
<s-sub-menu>
<v-tab to="/projects/" nuxt exact>Active Projects</v-tab>
<v-tab to="/projects/finished/" nuxt>Finished Projects</v-tab>
<v-tab v-if="archivingEnabled" to="/projects/archived/" nuxt>Archived Projects</v-tab>
</s-sub-menu>
<full-height-page>
<template #header>
<s-sub-menu class="flex-grow-0">
<v-tab to="/projects/" nuxt exact>Active Projects</v-tab>
<v-tab to="/projects/finished/" nuxt>Finished Projects</v-tab>
<v-tab v-if="archivingEnabled" to="/projects/archived/" nuxt>Archived Projects</v-tab>
</s-sub-menu>
</template>
<list-view url="/pentestprojects/?readonly=true">
<template #title>Finished Projects</template>
@ -12,7 +14,7 @@
<project-list-item :item="item" />
</template>
</list-view>
</div>
</full-height-page>
</template>
<script>

View File

@ -1,25 +1,29 @@
<template>
<div>
<s-sub-menu>
<v-tab :to="`/projects/`" nuxt exact>Active Projects</v-tab>
<v-tab :to="`/projects/finished/`" nuxt>Finished Projects</v-tab>
<v-tab v-if="archivingEnabled" to="/projects/archived/" nuxt>Archived Projects</v-tab>
</s-sub-menu>
<file-drop-area @drop="$refs.importBtn.performImport($event)" class="h-100">
<full-height-page>
<template #header>
<s-sub-menu>
<v-tab :to="`/projects/`" nuxt exact>Active Projects</v-tab>
<v-tab :to="`/projects/finished/`" nuxt>Finished Projects</v-tab>
<v-tab v-if="archivingEnabled" to="/projects/archived/" nuxt>Archived Projects</v-tab>
</s-sub-menu>
</template>
<list-view url="/pentestprojects/?readonly=false">
<template #title>Projects</template>
<template #actions>
<s-btn to="/projects/new/" nuxt color="primary" class="ml-1 mr-1">
<v-icon>mdi-plus</v-icon>
Create
</s-btn>
<btn-import :import="performImport" />
</template>
<template #item="{item}">
<project-list-item :item="item" />
</template>
</list-view>
</div>
<list-view url="/pentestprojects/?readonly=false">
<template #title>Projects</template>
<template #actions>
<s-btn to="/projects/new/" nuxt color="primary" class="ml-1 mr-1">
<v-icon>mdi-plus</v-icon>
Create
</s-btn>
<btn-import ref="importBtn" :import="performImport" />
</template>
<template #item="{item}">
<project-list-item :item="item" />
</template>
</list-view>
</full-height-page>
</file-drop-area>
</template>
<script>

Some files were not shown because too many files have changed in this diff Show More