synchronize git histories to allow pushing all commits in the future
This commit is contained in:
parent
3fe5f7f4fb
commit
599f396cb0
|
@ -3,3 +3,4 @@ app.env
|
|||
**/app.env
|
||||
.DS_Store
|
||||
.scannerwork
|
||||
.vscode
|
||||
|
|
|
@ -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
|
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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
|
||||
|
|
|
@ -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
2801
api/NOTICE
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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"
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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 \
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 | {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()]),
|
||||
),
|
||||
]
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')),
|
||||
])
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
BACKUP_KEY=""
|
|
@ -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 |
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
*
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
:label="label"
|
||||
:disabled="disabled"
|
||||
clearable
|
||||
class="mt-4"
|
||||
/>
|
||||
<s-combobox
|
||||
v-else-if="definition.type === 'combobox'"
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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: []
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-container>
|
||||
<v-container class="pt-0">
|
||||
<edit-toolbar>
|
||||
<template #title>{{ archive.name }}</template>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue