diff --git a/.github/workflows/push-tag-rc.yml b/.github/workflows/push-tag-rc.yml index 729275b1..4ee10317 100644 --- a/.github/workflows/push-tag-rc.yml +++ b/.github/workflows/push-tag-rc.yml @@ -21,9 +21,17 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Extract tag version + - name: Extract metadata and set variables id: vars - run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + run: | + RAW_TAG=${GITHUB_REF#refs/tags/} + CLEAN_TAG=${RAW_TAG#v} + COMMIT=$(git rev-parse --short HEAD) + BRANCH=$(git branch -r --contains "$GITHUB_SHA" | grep -v '\->' | head -n 1 | sed 's|origin/||') + echo "TAG=$CLEAN_TAG" >> $GITHUB_OUTPUT + echo "RAW_TAG=$RAW_TAG" >> $GITHUB_OUTPUT + echo "COMMIT=$COMMIT" >> $GITHUB_OUTPUT + echo "BRANCH=$BRANCH" >> $GITHUB_OUTPUT - name: Build and push backend image for ngen-django release candidate uses: docker/build-push-action@v5 @@ -32,6 +40,13 @@ jobs: file: docker/Dockerfile.prod push: true tags: certunlp/ngen-django:${{ steps.vars.outputs.TAG }} + build-args: | + APP_VERSION_TAG=${{ steps.vars.outputs.RAW_TAG }} + APP_COMMIT=${{ steps.vars.outputs.COMMIT }} + APP_BRANCH=${{ steps.vars.outputs.BRANCH }} + labels: | + org.opencontainers.image.version=${{ steps.vars.outputs.RAW_TAG }} + org.opencontainers.image.revision=${{ steps.vars.outputs.COMMIT }} - name: Build and push backend image for ngen release candidate uses: docker/build-push-action@v5 @@ -40,6 +55,13 @@ jobs: file: docker/Dockerfile.prod push: true tags: certunlp/ngen:${{ steps.vars.outputs.TAG }} + build-args: | + APP_VERSION_TAG=${{ steps.vars.outputs.RAW_TAG }} + APP_COMMIT=${{ steps.vars.outputs.COMMIT }} + APP_BRANCH=${{ steps.vars.outputs.BRANCH }} + labels: | + org.opencontainers.image.version=${{ steps.vars.outputs.RAW_TAG }} + org.opencontainers.image.revision=${{ steps.vars.outputs.COMMIT }} - name: Build and push frontend image release candidate uses: docker/build-push-action@v5 @@ -48,3 +70,10 @@ jobs: file: frontend/Dockerfile.prod push: true tags: certunlp/ngen-frontend:${{ steps.vars.outputs.TAG }} + build-args: | + APP_VERSION_TAG=${{ steps.vars.outputs.RAW_TAG }} + APP_COMMIT=${{ steps.vars.outputs.COMMIT }} + APP_BRANCH=${{ steps.vars.outputs.BRANCH }} + labels: | + org.opencontainers.image.version=${{ steps.vars.outputs.RAW_TAG }} + org.opencontainers.image.revision=${{ steps.vars.outputs.COMMIT }} diff --git a/.github/workflows/push-tag.yml b/.github/workflows/push-tag.yml index 97ba7e35..02c24dad 100644 --- a/.github/workflows/push-tag.yml +++ b/.github/workflows/push-tag.yml @@ -21,12 +21,17 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Extract tag version + - name: Extract metadata and set variables id: vars run: | RAW_TAG=${GITHUB_REF#refs/tags/} - CLEAN_TAG=${RAW_TAG#v} # Elimina la v si está al principio + CLEAN_TAG=${RAW_TAG#v} + COMMIT=$(git rev-parse --short HEAD) + BRANCH=$(git branch -r --contains "$GITHUB_SHA" | grep -v '\->' | head -n 1 | sed 's|origin/||') echo "TAG=$CLEAN_TAG" >> $GITHUB_OUTPUT + echo "RAW_TAG=$RAW_TAG" >> $GITHUB_OUTPUT + echo "COMMIT=$COMMIT" >> $GITHUB_OUTPUT + echo "BRANCH=$BRANCH" >> $GITHUB_OUTPUT - name: Build and push backend image for ngen-django uses: docker/build-push-action@v5 @@ -35,6 +40,13 @@ jobs: file: docker/Dockerfile.prod push: true tags: certunlp/ngen-django:latest,certunlp/ngen-django:${{ steps.vars.outputs.TAG }} + build-args: | + APP_VERSION_TAG=${{ steps.vars.outputs.RAW_TAG }} + APP_COMMIT=${{ steps.vars.outputs.COMMIT }} + APP_BRANCH=${{ steps.vars.outputs.BRANCH }} + labels: | + org.opencontainers.image.version=${{ steps.vars.outputs.RAW_TAG }} + org.opencontainers.image.revision=${{ steps.vars.outputs.COMMIT }} - name: Build and push backend image for ngen uses: docker/build-push-action@v5 @@ -43,6 +55,13 @@ jobs: file: docker/Dockerfile.prod push: true tags: certunlp/ngen:latest,certunlp/ngen:${{ steps.vars.outputs.TAG }} + build-args: | + APP_VERSION_TAG=${{ steps.vars.outputs.RAW_TAG }} + APP_COMMIT=${{ steps.vars.outputs.COMMIT }} + APP_BRANCH=${{ steps.vars.outputs.BRANCH }} + labels: | + org.opencontainers.image.version=${{ steps.vars.outputs.RAW_TAG }} + org.opencontainers.image.revision=${{ steps.vars.outputs.COMMIT }} - name: Build and push frontend image uses: docker/build-push-action@v5 @@ -51,3 +70,10 @@ jobs: file: frontend/Dockerfile.prod push: true tags: certunlp/ngen-frontend:latest,certunlp/ngen-frontend:${{ steps.vars.outputs.TAG }} + build-args: | + APP_VERSION_TAG=${{ steps.vars.outputs.RAW_TAG }} + APP_COMMIT=${{ steps.vars.outputs.COMMIT }} + APP_BRANCH=${{ steps.vars.outputs.BRANCH }} + labels: | + org.opencontainers.image.version=${{ steps.vars.outputs.RAW_TAG }} + org.opencontainers.image.revision=${{ steps.vars.outputs.COMMIT }} diff --git a/docker/.env/ngen.base.env b/docker/.env/ngen.base.env index 53518a07..36985ec2 100644 --- a/docker/.env/ngen.base.env +++ b/docker/.env/ngen.base.env @@ -52,11 +52,11 @@ NGEN_LANG_EXTERNAL=en PAGE_SIZE=10 PAGE_SIZE_MAX=100 -ALLOWED_FIELDS_BLOCKED_CASE=parent,state,tlp -ALLOWED_FIELDS_BLOCKED_EVENT=parent,case,tlp +BLOCKED_FIELDS_CASE= +BLOCKED_FIELDS_EVENT= ALLOWED_FIELDS_MERGED_CASE=parent,state,tlp ALLOWED_FIELDS_MERGED_EVENT=parent,case,tlp,network,sid -ALLOWED_FIELDS_BLOCKED_EXCEPTION=True +BLOCKED_FIELDS_EXCEPTION=True PRIORITY_ATTEND_TIME_DEFAULT=10080 PRIORITY_SOLVE_TIME_DEFAULT=10080 diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev index 98184d85..18680fdc 100644 --- a/docker/Dockerfile.dev +++ b/docker/Dockerfile.dev @@ -9,5 +9,15 @@ RUN apk add gettext libmagic gcc musl-dev python3-dev COPY . /code/ +# Add version information +ARG APP_VERSION_TAG +ARG APP_COMMIT +ARG APP_BRANCH +ENV APP_VERSION_TAG=$APP_VERSION_TAG +ENV APP_COMMIT=$APP_COMMIT +ENV APP_BRANCH=$APP_BRANCH +ENV APP_BUILD_FILE="dev" +RUN echo "{\"tag\": \"$APP_VERSION_TAG\", \"commit\": \"$APP_COMMIT\", \"branch\": \"$APP_BRANCH\", \"build_file\": \"$APP_BUILD_FILE\"}" > /code/version.json + # Must be built from the project root directory RUN pip install -r requirements.txt diff --git a/docker/Dockerfile.prod b/docker/Dockerfile.prod index c6fe8e66..2eaca93b 100644 --- a/docker/Dockerfile.prod +++ b/docker/Dockerfile.prod @@ -1,6 +1,15 @@ # Base stage for dependency installation FROM python:3.11-alpine3.20 AS base +ARG APP_VERSION_TAG +ARG APP_COMMIT +ARG APP_BRANCH + +ENV APP_VERSION_TAG=$APP_VERSION_TAG +ENV APP_COMMIT=$APP_COMMIT +ENV APP_BRANCH=$APP_BRANCH +ENV APP_BUILD_FILE="prod" + # Configure environment variables for pip ENV PIP_NO_CACHE_DIR=true \ PYTHONDONTWRITEBYTECODE=1 \ @@ -37,6 +46,9 @@ COPY --from=base /install /usr/local # Copy the application code COPY . /code/ +# Add version information +RUN echo "{\"tag\": \"$APP_VERSION_TAG\", \"commit\": \"$APP_COMMIT\", \"branch\": \"$APP_BRANCH\", \"build_file\": \"$APP_BUILD_FILE\"}" > /code/version.json + # Expose port 8000 but we will use a reverse proxy to serve the app anyway EXPOSE 8000 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 0e8c069b..9ef9b6a9 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -1,6 +1,6 @@ services: ngen-frontend: - image: certunlp/ngen-frontend:rc-2.3.2 + image: certunlp/ngen-frontend:rc-2.3.8.2 restart: always volumes: - ./data_static:/app/staticfiles @@ -11,7 +11,7 @@ services: - "80:80" ngen-django: - image: certunlp/ngen-django:rc-2.3.2 + image: certunlp/ngen-django:rc-2.3.8.2 restart: always entrypoint: ./docker/entrypoint.sh command: gunicorn project.wsgi:application --bind 0.0.0.0:8000 @@ -27,7 +27,7 @@ services: - ngen-redis ngen-celery-worker: - image: certunlp/ngen-django:rc-2.3.2 + image: certunlp/ngen-django:rc-2.3.8.2 restart: always command: celery -A project worker -l warning env_file: @@ -39,7 +39,7 @@ services: - ngen-redis ngen-celery-beat: - image: certunlp/ngen-django:rc-2.3.2 + image: certunlp/ngen-django:rc-2.3.8.2 restart: always command: celery -A project beat -l warning env_file: diff --git a/frontend/Dockerfile.dev b/frontend/Dockerfile.dev index 070cd27a..e75b05d6 100644 --- a/frontend/Dockerfile.dev +++ b/frontend/Dockerfile.dev @@ -22,5 +22,15 @@ COPY . ./ # Expose port EXPOSE 3000 +# Add version information +ARG APP_VERSION_TAG +ARG APP_COMMIT +ARG APP_BRANCH +ENV VITE_APP_VERSION_TAG=$APP_VERSION_TAG +ENV VITE_APP_COMMIT=$APP_COMMIT +ENV VITE_APP_BRANCH=$APP_BRANCH +ENV VITE_APP_BUILD_FILE="dev" +RUN echo "{\"tag\": \"$VITE_APP_VERSION_TAG\", \"commit\": \"$VITE_APP_COMMIT\", \"branch\": \"$VITE_APP_BRANCH\", \"build_file\": \"$VITE_APP_BUILD_FILE\"}" > /app/public/version.json + # start app CMD ["npm", "run","start"] diff --git a/frontend/Dockerfile.prod b/frontend/Dockerfile.prod index d793fc48..71b9c193 100644 --- a/frontend/Dockerfile.prod +++ b/frontend/Dockerfile.prod @@ -4,6 +4,14 @@ FROM node:18-alpine AS build # set working directory WORKDIR /app +ARG APP_VERSION_TAG +ARG APP_COMMIT +ARG APP_BRANCH + +ENV VITE_APP_VERSION_TAG=$APP_VERSION_TAG +ENV VITE_APP_COMMIT=$APP_COMMIT +ENV VITE_APP_BRANCH=$APP_BRANCH +ENV VITE_APP_BUILD_FILE="prod" # add `/app/node_modules/.bin` to $PATH ENV PATH=/app/node_modules/.bin:$PATH @@ -37,6 +45,9 @@ COPY nginx-prod/nginx.conf.template.ssl /etc/nginx/nginx.conf.template.ssl COPY nginx-prod/nginx.conf.template.no-ssl /etc/nginx/nginx.conf.template.no-ssl COPY nginx-prod/docker-entrypoint.sh /docker-entrypoint.sh +# Add version information +RUN echo "{\"tag\": \"$VITE_APP_VERSION_TAG\", \"commit\": \"$VITE_APP_COMMIT\", \"branch\": \"$VITE_APP_BRANCH\", \"build_file\": \"$VITE_APP_BUILD_FILE\"}" > /usr/share/nginx/html/version.json + # Give permissions to the entry script RUN chmod +x /docker-entrypoint.sh diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 7294b303..5b7d14be 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -101,13 +101,14 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" @@ -447,18 +448,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", - "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -487,87 +488,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", - "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", + "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", "license": "MIT", "dependencies": { - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/parser": { - "version": "7.25.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", - "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz", + "integrity": "sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.25.2" + "@babel/types": "^7.27.1" }, "bin": { "parser": "bin/babel-parser.js" @@ -2051,26 +1990,23 @@ "license": "MIT" }, "node_modules/@babel/runtime": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", - "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", + "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", - "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz", + "integrity": "sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg==", "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.25.0", - "@babel/types": "^7.25.0" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -2095,14 +2031,13 @@ } }, "node_modules/@babel/types": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", - "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", + "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.24.8", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4404,9 +4339,9 @@ } }, "node_modules/axios": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", - "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -4714,21 +4649,6 @@ "node": ">=6" } }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -6873,15 +6793,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "dev": true, @@ -8120,9 +8031,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", - "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, "node_modules/picomatch": { @@ -8798,10 +8709,6 @@ "node": ">=4" } }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "license": "MIT" - }, "node_modules/regenerator-transform": { "version": "0.15.2", "dev": true, @@ -9513,13 +9420,6 @@ "version": "1.0.3", "license": "MIT" }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "devOptional": true, @@ -9815,9 +9715,9 @@ } }, "node_modules/vite": { - "version": "5.4.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz", - "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "license": "MIT", "dependencies": { "esbuild": "^0.21.3", diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 2b8a5153..c654a906 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -53,6 +53,7 @@ "filter.cidr_domain": "cidr or domain", "info.related": "Related information", "login.do_not_have_an_account": "Don't have an account?", + "menu.about": "About", "menu.cases": "Cases", "menu.config": "Configuration", "menu.constituency": "Constituencies", @@ -81,6 +82,7 @@ "menu.states": "States", "menu.taxonomies": "Taxonomies", "menu.taxonomygroups": "Taxonomy Groups", + "menu.analyzermappings": "Analyzer Mappings", "menu.templates": "Templates", "menu.tags": "Tags", "menu.tlp": "TLP", @@ -424,6 +426,7 @@ "validate.username": "Only letters, numbers and '@', '.' , '+', '-', '_' special characters are allowed", "validation.password": "A password is required", "validation.username": "An username is required", + "w.about": "About", "w.active": "Active", "w.add": "Add", "w.assigned": "Assigned", @@ -457,6 +460,7 @@ "w.modify": "Modify", "w.nextState": "Next State", "w.no": "No", + "w.not_available": "Not available", "w.not_assigned": "Not assigned", "w.permissions": "Permissions", "w.posteriorState": "Posterior State", @@ -509,5 +513,10 @@ "ngen.retest.refresh": "Refresh", "ngen.retest.no_analyzer_mapping": "There is no analyzer mapping related to the event's taxonomy.", "ngen.retest.refresh.success": "Retests table refreshed successfully!", - "ngen.retest.refresh.error": "Failed to refresh retests table." + "ngen.retest.refresh.error": "Failed to refresh retests table.", + "ngen.analyzer_mapping.mapping_from": "Taxonomy", + "ngen.analyzer_mapping.mapping_to": "Mapping to", + "ngen.analyzer_mapping.analyzer_type": "Analyzer", + "ngen.analyzer_mapping.details": "Details", + "ngen.analyzer_mapping": "Analyzer mapping" } diff --git a/frontend/public/locales/es/translation.json b/frontend/public/locales/es/translation.json index b80e6704..cc41d1e5 100644 --- a/frontend/public/locales/es/translation.json +++ b/frontend/public/locales/es/translation.json @@ -53,6 +53,7 @@ "filter.cidr_domain": "CIDR o dominio", "info.related": "Información relacionada", "login.do_not_have_an_account": "¿No tienes una cuenta?", + "menu.about": "Acerca de", "menu.cases": "Casos", "menu.config": "Configuración", "menu.constituency": "Comunidades", @@ -81,6 +82,7 @@ "menu.states": "Estados", "menu.taxonomies": "Taxonomías", "menu.taxonomygroups": "Grupos de Taxonomías", + "menu.analyzermappings": "Mapeo de Analizadores", "menu.templates": "Plantillas", "menu.tags": "Etiquetas", "menu.tlp": "TLP", @@ -423,6 +425,7 @@ "validate.username": "Solo se permiten letras, números y los caracteres especiales '@', '.', '+', '-', '_'", "validation.password": "Se requiere una contraseña", "validation.username": "Se requiere un nombre de usuario", + "w.about": "Acerca de", "w.active": "Activo", "w.add": "Agregar", "w.assigned": "Asignado", @@ -456,6 +459,7 @@ "w.modify": "Modificar", "w.nextState": "Siguiente estado", "w.no": "No", + "w.not_available": "No disponible", "w.not_assigned": "No asignado", "w.permissions": "Permisos", "w.posteriorState": "Estado posterior", @@ -505,8 +509,13 @@ "ngen.retest.create": "Retest", "ngen.retest.success": "Retest realizado satisfactoriamente!", "ngen.retest.error": "Hubo un error al realizar el retest", - "ngen.retest.refresh": "Actualizar", - "ngen.retest.no_analyzer_mapping": "No hay ningun analizador mapeado para la taxonomia del evento", + "ngen.retest.refresh": "Actualizar", + "ngen.retest.no_analyzer_mapping": "No hay ningun analizador mapeado para la taxonomia del evento", "ngen.retest.refresh.success": "Tabla de retests actualizada con éxito.", - "ngen.retest.refresh.error": "No se pudo actualizar la tabla de retests." + "ngen.retest.refresh.error": "No se pudo actualizar la tabla de retests.", + "ngen.analyzer_mapping.mapping_from": "Taxonomia", + "ngen.analyzer_mapping.mapping_to": "Mapeado a", + "ngen.analyzer_mapping.analyzer_type": "Analizador", + "ngen.analyzer_mapping.details": "Detalles", + "ngen.analyzer_mapping": "Mapeo de analizador" } diff --git a/frontend/public/version.json b/frontend/public/version.json new file mode 100644 index 00000000..7a5adfc6 --- /dev/null +++ b/frontend/public/version.json @@ -0,0 +1 @@ +{"tag": "rc-2.3.8.2", "commit": "cf3e0dc", "branch": "develop", "build_file": "dev"} diff --git a/frontend/src/api/services/about.jsx b/frontend/src/api/services/about.jsx new file mode 100644 index 00000000..d7a18b00 --- /dev/null +++ b/frontend/src/api/services/about.jsx @@ -0,0 +1,17 @@ +import apiInstance from "../api"; +import { COMPONENT_URL } from "config/constant"; +import setAlert from "utils/setAlert"; + +const getVersion = async () => { + try { + const response = await apiInstance.get(COMPONENT_URL.version); + return response.data; + } catch (error) { + const message = error?.response?.data?.detail || "No se pudo recuperar la información de la aplicación"; + setAlert(message, "error"); + console.error("Error al obtener la versión:", error); + throw error; + } +}; + +export { getVersion }; diff --git a/frontend/src/api/services/analyzerMapping.jsx b/frontend/src/api/services/analyzerMapping.jsx index a77b7c6f..e53c62fe 100644 --- a/frontend/src/api/services/analyzerMapping.jsx +++ b/frontend/src/api/services/analyzerMapping.jsx @@ -1,17 +1,133 @@ import apiInstance from "../api"; -import { COMPONENT_URL } from "../../config/constant"; +import { COMPONENT_URL, PAGE } from "../../config/constant"; +import setAlert from "../../utils/setAlert"; -const getAnalyzerMappings = () => { - return apiInstance - .get(COMPONENT_URL.analyzerMapping) - .then((response) => { - return response.data; - }) - .catch((error) => { - return Promise.reject(error); - }); - }; +const getAllAnalyzerMappings = () => { + return apiInstance + .get(COMPONENT_URL.analyzerMapping) + .then((response) => { + return response.data; + }) + .catch((error) => { + return Promise.reject(error); + }); +}; + + +const getAnalyzerMappings = (currentPage, filters, order) => { + let messageError = `No se pudo recuperar la informacion de los mapeos`; + return apiInstance + .get(COMPONENT_URL.analyzerMapping + PAGE + currentPage + "&ordering=" + order + "&" + filters) + .then((response) => { + return response; + }) + .catch((error) => { + setAlert(messageError, "error", "analyzermapping"); + return Promise.reject(error); + }); +}; + + +const postAnalyzerMapping = (data) => { + const messageError = `Ya existe un mapeo con los mismos valores.`; + const messageSuccess = `El mapeo ha sido creado correctamente.`; + + const filters = `mapping_to__icontains=${data.mapping_to}&mapping_from__name__icontains=${data.mapping_from_name}&analyzer_type=${data.analyzer_type}`; + return getAnalyzerMappings(1, filters, "date") + .then((response) => { + if (response.data.results.length > 0) { + setAlert(messageError, "error", "analyzermapping"); + return Promise.reject(new Error(messageError)); + } + + return apiInstance + .post(COMPONENT_URL.analyzerMapping, { + mapping_to: data.mapping_to, + mapping_from: data.mapping_from, + analyzer_type: data.analyzer_type, + }) + .then((response) => { + setAlert(messageSuccess, "success", "analyzermapping"); + return response; + }); + }) + .catch((error) => { + return Promise.reject(error); + }); +}; + + +const getAnalyzerMapping = (url) => { + let messageError = `No se pudo recuperar la informacion del mapeo`; + return apiInstance + .get(url) + .then((response) => { + return apiInstance + .get(response.data.mapping_from) + .then((mappingResponse) => { + response.data.mapping_from_name = mappingResponse.data.name; + return response; + }); + }) + .catch((error) => { + setAlert(messageError, "error", "analyzermapping"); + return Promise.reject(error); + }); +}; + +const putAnalyzerMapping = (url, data) => { + const messageSuccess = `El mapeo ha sido editado correctamente.`; + const messageError = `Ya existe un mapeo con los mismos valores.`; + const notFoundError = `El mapeo no se ha encontrado.`; + + const filters = `mapping_to__icontains=${data.mapping_to}&mapping_from__name__icontains=${data.mapping_from_name}&analyzer_type=${data.analyzer_type}`; + return getAnalyzerMappings(1, filters, "date") + .then((response) => { + if (response.data.results.length > 0) { + setAlert(messageError, "error", "analyzermapping"); + return Promise.reject(new Error(messageError)); + } + + return apiInstance + .put(url, { + mapping_to: data.mapping_to, + mapping_from: data.mapping_from, + analyzer_type: data.analyzer_type, + }) + .then((response) => { + setAlert(messageSuccess, "success", "analyzermapping"); + return response; + }); + }) + .catch((error) => { + if (error.response?.detail === "Not found") { + setAlert(notFoundError, "error", "analyzermapping"); + } else { + setAlert(messageError, "error", "analyzermapping"); + } + return Promise.reject(error); + }); +}; + + +const deleteAnalyzerMapping = (url, name_from, name_to, analyzer) => { + let messageSuccess = `El mapeo de ${name_from} a ${name_to} del analizador ${analyzer} ha sido eliminado.`; + let messageError = `El mapeo de ${name_from} a ${name_to} del analizador ${analyzer} no se ha encontrado.`; + return apiInstance + .delete(url) + .then((response) => { + setAlert(messageSuccess, "success", "analyzerMapping"); + return response; + }) + .catch((error) => { + if (error.response.detail && error.response.detail === "Not found") { + messageError = `El mapeo de ${name_from} a ${name_to} del analizador ${analyzer} no se ha encontrado.`; + } + setAlert(messageError, "error", "analyzerMapping"); + return Promise.reject(error); + }); +}; -export { getAnalyzerMappings }; \ No newline at end of file +export { getAllAnalyzerMappings, getAnalyzerMappings, postAnalyzerMapping, getAnalyzerMapping, putAnalyzerMapping, deleteAnalyzerMapping }; \ No newline at end of file diff --git a/frontend/src/api/services/eventAnalysis.jsx b/frontend/src/api/services/eventAnalysis.jsx index 13891dbe..98a18282 100644 --- a/frontend/src/api/services/eventAnalysis.jsx +++ b/frontend/src/api/services/eventAnalysis.jsx @@ -27,7 +27,6 @@ const getRetests = (eventUrl, suppressAlert) => { const postRetest = (eventId) => { const messageSuccess = i18next.t("ngen.retest.success"); const messageError = i18next.t("ngen.retest.error"); - return apiInstance .post(`${COMPONENT_URL.event}${eventId}/retest/`, {}) .then((response) => { diff --git a/frontend/src/config/constant.jsx b/frontend/src/config/constant.jsx index a59f027e..3c3a2be3 100644 --- a/frontend/src/config/constant.jsx +++ b/frontend/src/config/constant.jsx @@ -1,5 +1,10 @@ export const BASE_URL = import.meta.env.VITE_APP_BASE_URL || "/home"; export const BASENAME = import.meta.env.VITE_APP_BASENAME || ""; +export const MODE = import.meta.env.MODE || "development"; +export const APP_VERSION_TAG = import.meta.env.VITE_APP_VERSION_TAG || 'unknown'; +export const APP_COMMIT = import.meta.env.VITE_APP_COMMIT || 'unknown'; +export const APP_BRANCH = import.meta.env.VITE_APP_BRANCH || 'unknown'; +export const APP_BUILD_FILE = import.meta.env.VITE_APP_BUILD_FILE || "unknown"; export const BASE_TITLE = " | ngen "; export const PAGE = "?page="; @@ -96,4 +101,5 @@ export const COMPONENT_URL = { tag: "tag/", eventAnalysis: "eventanalysis/", analyzerMapping: "analyzermapping/", + version: "version/", }; diff --git a/frontend/src/menu-items.jsx b/frontend/src/menu-items.jsx index 4455491b..f937253b 100644 --- a/frontend/src/menu-items.jsx +++ b/frontend/src/menu-items.jsx @@ -201,6 +201,14 @@ const menuItems = { icon: "", breadcrumbs: true }, + { + id: "analyzerMapping", + title: "menu.analyzermappings", + type: "item", + url: "/analyzermappings", + icon: "", + breadcrumbs: true + }, { id: "state", title: "menu.states", @@ -236,6 +244,15 @@ const menuItems = { classes: "", icon: "", breadcrumbs: true + }, + { + id: "about", + title: "menu.about", + type: "item", + url: "/about", + classes: "", + icon: "", + breadcrumbs: true } ] } diff --git a/frontend/src/routes.jsx b/frontend/src/routes.jsx index 844ca622..bdf73c99 100644 --- a/frontend/src/routes.jsx +++ b/frontend/src/routes.jsx @@ -150,6 +150,30 @@ const routes = [ permissions: ["change_taxonomygroup"], element: lazy(() => import("./views/taxonomyGroup/EditTaxonomyGroup")) }, + { + exact: "true", + path: "/analyzermappings", + layout: AdminLayout, + guard: PermissionGuard, + permissions: ["view_analyzermapping"], + element: lazy(() => import("./views/analyzerMapping/ListAnalyzerMappings")) + }, + { + exact: "true", + path: "/analyzermappings/create", + layout: AdminLayout, + guard: PermissionGuard, + permissions: ["add_analyzermapping"], + element: lazy(() => import("./views/analyzerMapping/CreateAnalyzerMapping")) + }, + { + exact: "true", + path: "/analyzermappings/edit/:id", + layout: AdminLayout, + guard: PermissionGuard, + permissions: ["change_analyzermapping"], + element: lazy(() => import("./views/analyzerMapping/EditAnalyzerMapping")) + }, { exact: "true", path: "/tlp", @@ -624,6 +648,13 @@ const routes = [ element: lazy(() => import("./views/network/EditNetwork")), routeParams: { asNetworkAdmin: true, basePath: "/networkadmin" } }, + { + exact: "true", + path: "/about", + layout: AdminLayout, + guard: AuthGuard, + element: lazy(() => import("./views/home/About.jsx")), + }, { path: "*", exact: "true", diff --git a/frontend/src/views/analyzerMapping/CreateAnalyzerMapping.jsx b/frontend/src/views/analyzerMapping/CreateAnalyzerMapping.jsx new file mode 100644 index 00000000..aaf9a81f --- /dev/null +++ b/frontend/src/views/analyzerMapping/CreateAnalyzerMapping.jsx @@ -0,0 +1,131 @@ +import React, { useEffect, useState } from "react"; +import { Button, Card, Col, Form, Row } from "react-bootstrap"; +import { getMinifiedTaxonomy } from "../../api/services/taxonomies"; +import { postAnalyzerMapping } from "../../api/services/analyzerMapping"; +import SelectLabel from "../../components/Select/SelectLabel"; +import { useTranslation } from "react-i18next"; +import CrudButton from "../../components/Button/CrudButton"; + +const CreateAnalyzerMapping = () => { + const [mappingFrom, setMappingFrom] = useState(""); + const [mappingTo, setMappingTo] = useState(""); + const [analyzerType, setAnalyzerType] = useState(""); + const [taxonomies, setTaxonomies] = useState([]); + const [selectedMappingFrom, setSelectedMappingFrom] = useState(null); + const [showAlert, setShowAlert] = useState(false); + const { t } = useTranslation(); + + useEffect(() => { + getMinifiedTaxonomy() + .then((response) => { + const listTaxonomies = response.map((taxonomy) => ({ + value: taxonomy.url, + label: taxonomy.name, + })); + setTaxonomies(listTaxonomies); + }) + .catch((error) => { + console.error("Error fetching taxonomies:", error); + }); + }, []); + + const createAnalyzerMapping = () => { + let data = { + mapping_from: mappingFrom, + mapping_from_name: selectedMappingFrom.label, + mapping_to: mappingTo, + analyzer_type: analyzerType, + }; + postAnalyzerMapping(data) + .then(() => { + window.location.href = "/analyzermappings"; + }) + .catch((error) => { + + console.error("Error creating analyzer mapping:", error); + setShowAlert(true); + }); + }; + + const resetShowAlert = () => { + setShowAlert(false); + }; + + return ( + + + + + + {t("ngen.analyzer_mapping")} + + +
+ + + + + + + + {t("ngen.analyzer_mapping.mapping_to")} * + + setMappingTo(e.target.value)} + isInvalid={mappingTo === ""} + /> + {mappingTo === "" && ( +
{t("ngen.analyzer_mapping.mapping_to") + " invalid"}
+ )} +
+ +
+ + + + + {t("ngen.analyzer_mapping.analyzer_type")} * + + setAnalyzerType(e.target.value)} + isInvalid={analyzerType === ""} + /> + {analyzerType === "" && ( +
{t("ngen.analyzer_mapping.analyzer_type") + " invalid"}
+ )} +
+ +
+ + {mappingFrom && mappingTo && analyzerType ? ( + + ) : ( + + )} + + +
+
+
+ +
+
+ ); +}; + +export default CreateAnalyzerMapping; \ No newline at end of file diff --git a/frontend/src/views/analyzerMapping/EditAnalyzerMapping.jsx b/frontend/src/views/analyzerMapping/EditAnalyzerMapping.jsx new file mode 100644 index 00000000..6d52cbeb --- /dev/null +++ b/frontend/src/views/analyzerMapping/EditAnalyzerMapping.jsx @@ -0,0 +1,166 @@ +import React, { useEffect, useState } from "react"; +import { Button, Card, Col, Form, Row, Spinner } from "react-bootstrap"; +import { useParams } from "react-router-dom"; +import { getMinifiedTaxonomy } from "../../api/services/taxonomies"; +import { getAnalyzerMapping, putAnalyzerMapping } from "../../api/services/analyzerMapping"; +import SelectLabel from "../../components/Select/SelectLabel"; +import { useTranslation } from "react-i18next"; +import CrudButton from "../../components/Button/CrudButton"; +import { COMPONENT_URL } from "config/constant"; + +const EditAnalyzerMapping = () => { + const { id } = useParams(); + const [mappingFrom, setMappingFrom] = useState(""); + const [mappingTo, setMappingTo] = useState(""); + const [analyzerType, setAnalyzerType] = useState(""); + const [taxonomies, setTaxonomies] = useState([]); + const [selectedMappingFrom, setSelectedMappingFrom] = useState(null); + const [loading, setLoading] = useState(true); + const [showAlert, setShowAlert] = useState(false); + const { t } = useTranslation(); + + + useEffect(() => { + getMinifiedTaxonomy() + .then((response) => { + const listTaxonomies = response.map((taxonomy) => ({ + value: taxonomy.url, + label: taxonomy.name, + })); + setTaxonomies(listTaxonomies); + }) + .catch((error) => { + console.log("Error fetching taxonomies:", error); + }); + }, []); + + + useEffect(() => { + getAnalyzerMapping(COMPONENT_URL.analyzerMapping + id + "/") + .then((response) => { + setMappingFrom(response.data.mapping_from); + setMappingTo(response.data.mapping_to); + setAnalyzerType(response.data.analyzer_type); + setSelectedMappingFrom({ + value: response.data.mapping_from, + label: response.data.mapping_from_name, + }); + }) + .catch((error) => { + console.log("Error fetching analyzer mapping:", error); + }) + .finally(() => { + setLoading(false); + setShowAlert(true); + }); + }, [id]); + + + const editAnalyzerMapping = () => { + const data = { + mapping_from: mappingFrom, + mapping_from_name: selectedMappingFrom.label, + mapping_to: mappingTo, + analyzer_type: analyzerType, + }; + + putAnalyzerMapping(COMPONENT_URL.analyzerMapping + id + "/", data) + .then(() => { + window.location.href = "/analyzermappings"; + }) + .catch((error) => { + console.log("Error updating analyzer mapping:", error); + setShowAlert(true); + }); + }; + + const resetShowAlert = () => { + setShowAlert(false); + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + + + + {t("ngen.analyzer_mapping")} + + +
+ + + + + + + + {t("ngen.analyzer_mapping.mapping_to")} * + + setMappingTo(e.target.value)} + isInvalid={mappingTo === ""} + /> + {mappingTo === "" && ( +
{t("ngen.analyzer_mapping.mapping_to") + " invalid"}
+ )} +
+ +
+ + + + + {t("ngen.analyzer_mapping.analyzer_type")} * + + setAnalyzerType(e.target.value)} + isInvalid={analyzerType === ""} + /> + {analyzerType === "" && ( +
{t("ngen.analyzer_mapping.analyzer_type") + " invalid"}
+ )} +
+ +
+ + {mappingFrom && mappingTo && analyzerType ? ( + + ) : ( + + )} + + +
+
+
+ +
+
+ ); +}; + +export default EditAnalyzerMapping; \ No newline at end of file diff --git a/frontend/src/views/analyzerMapping/ListAnalyzerMappings.jsx b/frontend/src/views/analyzerMapping/ListAnalyzerMappings.jsx new file mode 100644 index 00000000..3f4fb656 --- /dev/null +++ b/frontend/src/views/analyzerMapping/ListAnalyzerMappings.jsx @@ -0,0 +1,103 @@ +import React, { useEffect, useState } from "react"; +import { Card, Col, Row } from "react-bootstrap"; +import CrudButton from "../../components/Button/CrudButton"; +import AdvancedPagination from "../../components/Pagination/AdvancedPagination"; +import Search from "../../components/Search/Search"; +import TableAnalyzerMapping from "./components/TableAnalyzerMapping"; +import { useTranslation } from "react-i18next"; +import { getAnalyzerMappings } from "../../api/services/analyzerMapping"; + +const ListAnalyzerMappings = () => { + const [analyzerMappings, setAnalyzerMappings] = useState([]); + const [loading, setLoading] = useState(true); + const { t } = useTranslation(); + + const [currentPage, setCurrentPage] = useState(1); + const [countItems, setCountItems] = useState(0); + const [updatePagination, setUpdatePagination] = useState(false); + const [disabledPagination, setDisabledPagination] = useState(true); + const [wordToSearch, setWordToSearch] = useState(""); + + const [order, setOrder] = useState("date"); + + function updatePage(chosenPage) { + setCurrentPage(chosenPage); + } + + useEffect(() => { + setLoading(true); + getAnalyzerMappings(currentPage, wordToSearch, order) + .then((response) => { + setCountItems(response.data.count); + setAnalyzerMappings(response.data.results); + if (currentPage === 1) { + setUpdatePagination(true); + } + setDisabledPagination(false); + }) + .catch((error) => { + console.error("Error fetching analyzer mappings:", error); + }) + .finally(() => { + setLoading(false); + }); + }, [currentPage, order, wordToSearch]); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default ListAnalyzerMappings; \ No newline at end of file diff --git a/frontend/src/views/analyzerMapping/components/TableAnalyzerMapping.jsx b/frontend/src/views/analyzerMapping/components/TableAnalyzerMapping.jsx new file mode 100644 index 00000000..edae346b --- /dev/null +++ b/frontend/src/views/analyzerMapping/components/TableAnalyzerMapping.jsx @@ -0,0 +1,167 @@ +import React, { useState, useEffect } from "react"; +import { Row, Spinner, Table, Modal, Col, Card, CloseButton } from "react-bootstrap"; +import CrudButton from "../../../components/Button/CrudButton"; +import ModalConfirm from "../../../components/Modal/ModalConfirm"; +import DateShowField from "../../../components/Field/DateShowField"; +import { useTranslation } from "react-i18next"; +import { getTaxonomy } from "../../../api/services/taxonomies"; +import { deleteAnalyzerMapping, getAnalyzerMapping } from "../../../api/services/analyzerMapping"; + +const TableAnalyzerMapping = ({ list, loading, order, setOrder, setLoading }) => { + const [modalDelete, setModalDelete] = useState(false); + const [modalShow, setModalShow] = useState(false); + const [selectedMapping, setSelectedMapping] = useState(null); + const [url, setUrl] = useState(null); + const [taxonomyNames, setTaxonomyNames] = useState({}); + const { t } = useTranslation(); + + useEffect(() => { + const fetchTaxonomyNames = async () => { + const names = {}; + for (const mapping of list) { + try { + const response = await getTaxonomy(mapping.mapping_from); + names[mapping.mapping_from] = response.data.name; + } catch (error) { + console.error("Error fetching taxonomy name:", error); + names[mapping.mapping_from] = t("ngen.error"); + } + } + setTaxonomyNames(names); + }; + + fetchTaxonomyNames(); + }, [list]); + + if (loading) { + return ( + + + + ); + } + + const handleClose = () => setModalShow(false); + + const showAnalyzerMapping = (url) => { + getAnalyzerMapping(url) + .then((response) => { + setSelectedMapping({ + ...response.data, + mapping_from_name: taxonomyNames[response.data.mapping_from], + }); + setModalShow(true); + }) + .catch((error) => { + console.error("Error fetching analyzer mapping details:", error); + }); + }; + + const deleteMapping = (url) => { + deleteAnalyzerMapping(url, selectedMapping?.mapping_from_name, selectedMapping?.mapping_to, selectedMapping?.analyzer_type) + .then(() => { + setLoading(true); + setModalDelete(false); + }) + .catch((error) => { + console.error("Error deleting analyzer mapping:", error); + }); + }; + + return ( + + + + + + + + + + + + + {list.map((mapping, index) => { + const parts = mapping.url.split("/"); + let itemNumber = parts[parts.length - 2]; + return ( + + + + + + + + ); + })} + +
{t("ngen.analyzer_mapping.mapping_from")}{t("ngen.analyzer_mapping.mapping_to")}{t("ngen.analyzer_mapping.analyzer_type")}{t("ngen.date.created")}{t("ngen.options")}
{taxonomyNames[mapping.mapping_from]}{mapping.mapping_to}{mapping.analyzer_type} + + + showAnalyzerMapping(mapping.url)} /> + + { setSelectedMapping(mapping); setUrl(mapping.url); setModalDelete(true); }} /> +
+ + {/* Modal for Viewing Details */} + + + + + + + + + {t("ngen.analyzer_mapping.details")} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{t("ngen.analyzer_mapping.mapping_from")}{selectedMapping?.mapping_from_name}
{t("ngen.analyzer_mapping.mapping_to")}{selectedMapping?.mapping_to}
{t("ngen.analyzer_mapping.analyzer_type")}{selectedMapping?.analyzer_type}
{t("ngen.date.created")}{selectedMapping?.created}
{t("ngen.date.modified")}{selectedMapping?.modified}
+
+
+ +
+
+
+ + {/* Modal for Confirming Deletion */} + setModalDelete(false)} + ifConfirm={() => deleteMapping(url)} + /> +
+ ); +}; + +export default TableAnalyzerMapping; \ No newline at end of file diff --git a/frontend/src/views/case/components/TableCase.jsx b/frontend/src/views/case/components/TableCase.jsx index 093c49eb..c9204725 100644 --- a/frontend/src/views/case/components/TableCase.jsx +++ b/frontend/src/views/case/components/TableCase.jsx @@ -17,6 +17,7 @@ import EventComponent from "views/tanstackquery/EventComponent"; import TaxonomyComponent from "views/tanstackquery/TaxonomyComponent"; + const TableCase = ({ setIfModify, cases, diff --git a/frontend/src/views/event/components/SmallRetestTable.jsx b/frontend/src/views/event/components/SmallRetestTable.jsx index 3e7f1f39..e60c04fe 100644 --- a/frontend/src/views/event/components/SmallRetestTable.jsx +++ b/frontend/src/views/event/components/SmallRetestTable.jsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import TableRetests from "./TableRetests"; import { getRetests, postRetest } from "../../../api/services/eventAnalysis"; import Alert from "components/Alert/Alert"; -import { getAnalyzerMappings } from "../../../api/services/analyzerMapping"; +import { getAllAnalyzerMappings } from "../../../api/services/analyzerMapping"; const SmallRetestTable = ({ retests, eventId, eventUrl, taxonomyUrl }) => { const { t } = useTranslation(); @@ -27,7 +27,7 @@ const SmallRetestTable = ({ retests, eventId, eventUrl, taxonomyUrl }) => { useEffect(() => { const checkAnalyzerMapping = async () => { try { - const mappings = await getAnalyzerMappings(); + const mappings = await getAllAnalyzerMappings(); const isMapped = mappings.results.some( (mapping) => mapping.mapping_from === taxonomyUrl ); diff --git a/frontend/src/views/home/About.jsx b/frontend/src/views/home/About.jsx new file mode 100644 index 00000000..b29386a7 --- /dev/null +++ b/frontend/src/views/home/About.jsx @@ -0,0 +1,72 @@ +import React, { useEffect, useState } from "react"; +import { Row, Card } from "react-bootstrap"; +import { APP_COMMIT, APP_BRANCH, APP_BUILD_FILE, APP_VERSION_TAG, MODE } from "config/constant"; +import { getVersion } from "api/services/about"; +import { useTranslation } from "react-i18next"; + +const InfoSection = ({ title, data }) => { + const { t } = useTranslation(); + + return ( + + + {title} + + + {data && + Object.keys(data).map((field) => ( +

+ {field.replace("_", " ").replace(/\b\w/g, (l) => l.toUpperCase())}: {data[field] || t("w.not_available")} +

+ ))} +
+
+ ); +}; + +const About = () => { + const [about, setAbout] = useState({ backend: {} }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { t } = useTranslation(); + + useEffect(() => { + getVersion() + .then((value) => { + setAbout(value); + }) + .catch((error) => { + console.error("Error fetching version:", error); + setError(error); + }) + .finally(() => setLoading(false)); + }, []); + + if (loading || error) { + return ( +
+ {loading &&

Loading...

} + {error &&

Error: {error.message}

} +
+ ); + } + + return ( + <> +

{t("w.about")}

+ +
+ +
+
+ +
+
+ + ); +}; + +export default About; diff --git a/frontend/src/views/tanstackquery/EventComponent.jsx b/frontend/src/views/tanstackquery/EventComponent.jsx index 5d951f43..77b8d5ff 100644 --- a/frontend/src/views/tanstackquery/EventComponent.jsx +++ b/frontend/src/views/tanstackquery/EventComponent.jsx @@ -1,43 +1,35 @@ import React from "react"; import { useQuery } from '@tanstack/react-query'; -import { getQueryEvent } from "api/services/events"; +import { getEvent } from "api/services/events"; import LetterFormat from "../../components/LetterFormat"; - const EventComponent = ({ event }) => { - - - // Fetch event data using useQuery. + const { data, isLoading, error } = useQuery({ - queryKey: ['eventKey'], // Single query key to fetch all event data - queryFn: getQueryEvent, - + queryKey: ['eventKey', event], // Every event is identified by its url + queryFn: () => getEvent(event).then((res) => res.data), // getEvent receives an URL and retrieves an event + enabled: !!event, // Only runs if event is valid staleTime: 5 * 60 * 1000, - refetchOnWindowFocus: false, // Disable refetching when window is focused - refetchOnReconnect: false, // Disable refetching when the app reconnects + refetchOnWindowFocus: false, + refetchOnReconnect: false, }); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; - const selectedEvent = data?.[event]; - + const selectedEvent = data; return ( - -
- - - -
+
+ +
); -}; - +}; export default EventComponent; diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 38c9d8fd..ea3ed038 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -13,13 +13,14 @@ "@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/trace-mapping" "^0.3.24" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.24.7": - version "7.24.7" - resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz" - integrity sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA== +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.24.7", "@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== dependencies: - "@babel/highlight" "^7.24.7" - picocolors "^1.0.0" + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" "@babel/compat-data@^7.22.6", "@babel/compat-data@^7.24.4", "@babel/compat-data@^7.25.2": version "7.25.2" @@ -197,15 +198,15 @@ dependencies: "@babel/types" "^7.24.5" -"@babel/helper-string-parser@^7.24.8": - version "7.24.8" - resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz" - integrity sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ== +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== -"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.24.7": - version "7.24.7" - resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz" - integrity sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w== +"@babel/helper-validator-identifier@^7.22.20", "@babel/helper-validator-identifier@^7.24.7", "@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== "@babel/helper-validator-option@^7.23.5", "@babel/helper-validator-option@^7.24.7", "@babel/helper-validator-option@^7.24.8": version "7.24.8" @@ -220,29 +221,19 @@ "@babel/types" "^7.24.5" "@babel/helpers@^7.25.0": - version "7.25.0" - resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz" - integrity sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw== - dependencies: - "@babel/template" "^7.25.0" - "@babel/types" "^7.25.0" - -"@babel/highlight@^7.24.7": - version "7.24.7" - resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz" - integrity sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw== + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz" + integrity sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ== dependencies: - "@babel/helper-validator-identifier" "^7.24.7" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" + "@babel/template" "^7.27.1" + "@babel/types" "^7.27.1" -"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.25.0", "@babel/parser@^7.25.3": - version "7.25.3" - resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz" - integrity sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw== +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.25.0", "@babel/parser@^7.25.3", "@babel/parser@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.27.1.tgz" + integrity sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ== dependencies: - "@babel/types" "^7.25.2" + "@babel/types" "^7.27.1" "@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.5": version "7.24.5" @@ -907,20 +898,18 @@ version "0.8.0" "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.18.3", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.15", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.25.0" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz" - integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== - dependencies: - regenerator-runtime "^0.14.0" + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz" + integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog== -"@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.25.0": - version "7.25.0" - resolved "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz" - integrity sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q== +"@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.25.0", "@babel/template@^7.27.1": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.1.tgz" + integrity sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg== dependencies: - "@babel/code-frame" "^7.24.7" - "@babel/parser" "^7.25.0" - "@babel/types" "^7.25.0" + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.1" + "@babel/types" "^7.27.1" "@babel/traverse@^7.24.7", "@babel/traverse@^7.25.2": version "7.25.3" @@ -935,14 +924,13 @@ debug "^4.3.1" globals "^11.1.0" -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.5", "@babel/types@^7.24.7", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.4.4": - version "7.25.2" - resolved "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz" - integrity sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q== +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0", "@babel/types@^7.24.5", "@babel/types@^7.24.7", "@babel/types@^7.25.0", "@babel/types@^7.25.2", "@babel/types@^7.27.1", "@babel/types@^7.4.4": + version "7.27.1" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz" + integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q== dependencies: - "@babel/helper-string-parser" "^7.24.8" - "@babel/helper-validator-identifier" "^7.24.7" - to-fast-properties "^2.0.0" + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" "@cush/relative@^1.0.0": version "1.0.0" @@ -1727,13 +1715,6 @@ ansi-regex@^5.0.1: ansi-regex@^6.0.1: version "6.0.1" -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - ansi-styles@^4.0.0, ansi-styles@^4.1.0: version "4.3.0" dependencies: @@ -1871,9 +1852,9 @@ axe-core@^4.9.1: integrity sha512-Mr2ZakwQ7XUAjp7pAwQWRhhK8mQQ6JAaNWSjmjxil0R8BPioMtQsTLOolGYkji1rcL++3dCqZA3zWqpT+9Ew6g== axios@^1.7.3: - version "1.7.4" - resolved "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz" - integrity sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw== + version "1.9.0" + resolved "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz" + integrity sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg== dependencies: follow-redirects "^1.15.6" form-data "^4.0.0" @@ -1986,15 +1967,6 @@ caniuse-lite@^1.0.30001646: resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz" integrity sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg== -chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - chalk@^3.0.0: version "3.0.0" dependencies: @@ -2041,13 +2013,6 @@ clsx@^2.1.0: resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz" integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - color-convert@^2.0.1: version "2.0.1" dependencies: @@ -2056,11 +2021,6 @@ color-convert@^2.0.1: color-name@~1.1.4: version "1.1.4" -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" @@ -2677,11 +2637,6 @@ escalade@^3.1.2: resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz" integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - escape-string-regexp@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" @@ -3100,11 +3055,6 @@ graphemer@^1.4.0: has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - has-flag@^4.0.0: version "4.0.0" @@ -3720,10 +3670,10 @@ performance-now@^2.1.0: resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" integrity sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow== -picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz" - integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== +picocolors@^1.0.0, picocolors@^1.0.1, picocolors@^1.1.0, picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" @@ -4056,9 +4006,6 @@ regenerator-runtime@^0.13.9: resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== -regenerator-runtime@^0.14.0: - version "0.14.1" - regenerator-transform@^0.15.2: version "0.15.2" dependencies: @@ -4383,13 +4330,6 @@ supercluster@^8.0.1: dependencies: kdbush "^4.0.2" -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - supports-color@^7.1.0: version "7.2.0" dependencies: @@ -4425,9 +4365,6 @@ tiny-case@^1.0.3: tiny-warning@^1.0.2: version "1.0.3" -to-fast-properties@^2.0.0: - version "2.0.0" - to-regex-range@^5.0.1: version "5.0.1" dependencies: @@ -4585,9 +4522,9 @@ vite-jsconfig-paths@^2.0.1: tsconfig-paths "^3.9.0" "vite@^4.2.0 || ^5.0.0", vite@^5.4.1, vite@>2.0.0-0: - version "5.4.14" - resolved "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz" - integrity sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA== + version "5.4.19" + resolved "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz" + integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA== dependencies: esbuild "^0.21.3" postcss "^8.4.43" diff --git a/ngen/filters.py b/ngen/filters.py index 3f9ed389..378baf20 100644 --- a/ngen/filters.py +++ b/ngen/filters.py @@ -22,6 +22,8 @@ Network, Contact, Playbook, + AnalyzerMapping, + EventAnalysis ) from ngen.models.common.mixins import AddressManager @@ -513,3 +515,21 @@ class Meta: "active": ["exact"], "networks": ["exact"], } + + +class AnalyzerMappingFilter(BaseFilter): + """ + AnalyzerMapping model filter. + Allows to filter by: + - mapping_to (icontains) + - mapping_from__name (icontains) + - analyzer_type (exact) + """ + + class Meta: + model = AnalyzerMapping + fields = { + "mapping_to": ["icontains"], + "mapping_from__name": ["icontains"], + "analyzer_type": ["exact"], + } \ No newline at end of file diff --git a/ngen/models/case.py b/ngen/models/case.py index 61e8a4f7..c4bf5c48 100644 --- a/ngen/models/case.py +++ b/ngen/models/case.py @@ -222,8 +222,14 @@ def assigned_email(self): return self.assigned.email return None - @property - def team_email(self): + def get_team_email_by_priority(self): + """ + Get the team email if the priority (config.TEAM_EMAIL_PRIORITY) is + greater than or equal to the case priority. + Used by get_internal_contacts() method to send emails to the team on + bcc. + :return: team email or None + """ priority = Priority.objects.get(name=config.TEAM_EMAIL_PRIORITY) if config.TEAM_EMAIL and priority.severity >= self.priority.severity: return config.TEAM_EMAIL @@ -372,7 +378,7 @@ def subject_v2(self, channel_type: str = None) -> str: # template_params = self.template_params # recipients = self.recipients # team_recipients = [ - # mail for mail in [self.assigned_email, self.team_email] if mail + # mail for mail in [self.assigned_email, self.get_team_email_by_priority()] if mail # ] # for contacts, events in event_by_contacts.items(): @@ -441,6 +447,7 @@ def communicate_v2( subject=self.subject_v2("AFFECTED"), template=template, template_params=template_params, + bcc_recipients=self.get_internal_contacts(), attachments=( self.get_attachments_for_events_v2([event]) if send_attachments @@ -462,7 +469,12 @@ def communicate_v2( self.notification_count += 1 def get_internal_contacts(self): - return clean_list([self.assigned_email, self.team_email]) + """ + Returns a list of internal contacts of the case. + """ + # this function is not using get_team_email_by_priority() because can + # return None and this is not the expected behavior + return clean_list([self.assigned_email, config.TEAM_EMAIL]) def get_affected_contacts(self): contacts_from_all_events = [] diff --git a/ngen/models/common/mixins.py b/ngen/models/common/mixins.py index afe1b980..468688fd 100644 --- a/ngen/models/common/mixins.py +++ b/ngen/models/common/mixins.py @@ -24,7 +24,7 @@ from taggit.managers import TaggableManager import ngen -from ngen.utils import slugify_underscore +from ngen.utils import clean_list, slugify_underscore from .parsing import StringIdentifier, StringType @@ -141,18 +141,26 @@ def blocked(self) -> bool: raise NotImplementedError @property - def allowed_fields(self) -> bool: - key = f"ALLOWED_FIELDS_BLOCKED_{self.__class__.__name__.upper()}" - values = getattr(config, key, []).split(",") + def blocked_fields(self) -> list[str]: + """ + Fields that are blocked to be modified on blocked instances. + """ + key = f"BLOCKED_FIELDS_{self.__class__.__name__.upper()}" + values = getattr(config, key, "") or "" + values = values.split(",") values += [f"{v}_id" for v in values] - return values + return clean_list(values) @property - def allowed_fields_on_merged(self) -> bool: + def allowed_fields_on_merged(self) -> list[str]: + """ + Fields that are allowed to be modified on merged instances. + """ key = f"ALLOWED_FIELDS_MERGED_{self.__class__.__name__.upper()}" - values = getattr(config, key, []).split(",") + values = getattr(config, key, "") or "" + values = values.split(",") values += [f"{v}_id" for v in values] - return values + return clean_list(values) @property def mergeable(self) -> bool: @@ -236,8 +244,10 @@ def check_allowed_fields(self, exclude=None): if self.blocked: exceptions = {} for attr in self.__dict__: - if attr not in self.allowed_fields and self.has_changed(attr): - if config.ALLOWED_FIELDS_BLOCKED_EXCEPTION: + if attr in self.blocked_fields and str(self.initial_value(attr)) != str( + getattr(self, attr) + ): + if config.BLOCKED_FIELDS_EXCEPTION: exceptions[attr] = [ { "__all__": gettext_lazy( @@ -647,8 +657,9 @@ def artifact_update(self): based on the artifacts_dict property of the instance. """ if self.enrichable: + artifact_types = config.ALLOWED_ARTIFACTS_TYPES or "" for artifact_type, artifact_values in self.artifacts_dict.items(): - if artifact_type in config.ALLOWED_ARTIFACTS_TYPES.split(","): + if artifact_type in artifact_types.split(","): relations = [] for artifact_value in artifact_values: artifact, created = ngen.models.Artifact.objects.get_or_create( diff --git a/ngen/serializers/case.py b/ngen/serializers/case.py index 525f657a..b20c1108 100644 --- a/ngen/serializers/case.py +++ b/ngen/serializers/case.py @@ -109,8 +109,15 @@ def get_comments(self, obj): ) @staticmethod - def allowed_fields(): - return config.ALLOWED_FIELDS_BLOCKED_EVENT.split(",") + def blocked_fields() -> list: + """ + Returns a list of blocked fields for the EventSerializer. + The blocked fields are defined in the config.BLOCKED_FIELDS_EVENT setting. + If the setting is not defined, an empty list is returned. + """ + if not config.BLOCKED_FIELDS_EVENT: + return [] + return config.BLOCKED_FIELDS_EVENT.split(",") @staticmethod def not_allowed_fields(): @@ -145,7 +152,7 @@ def validate(self, attrs): # If the attribute is not allowed, we check if it's being modified if ( - config.ALLOWED_FIELDS_BLOCKED_EXCEPTION + config.BLOCKED_FIELDS_EXCEPTION and getattr(self.instance, attr, None) != attrs[attr] ): raise ValidationError( @@ -186,8 +193,10 @@ class EventSerializerReduced( ): @staticmethod - def allowed_fields(): - return config.ALLOWED_FIELDS_BLOCKED_EVENT.split(",") + def blocked_fields(): + if not config.BLOCKED_FIELDS_EVENT: + return [] + return config.BLOCKED_FIELDS_EVENT.split(",") class Meta: model = models.Event @@ -295,8 +304,15 @@ def get_evidence_events(self, obj): ) @staticmethod - def allowed_fields(): - return config.ALLOWED_FIELDS_BLOCKED_CASE.split(",") + def blocked_fields(): + """ + Returns a list of blocked fields for the CaseSerializer. + The blocked fields are defined in the config.BLOCKED_FIELDS_CASE setting. + If the setting is not defined, an empty list is returned. + """ + if not config.BLOCKED_FIELDS_CASE: + return [] + return config.BLOCKED_FIELDS_CASE.split(",") def get_comments(self, obj): comments_qs = Comment.objects.filter_parents_by_object(obj) @@ -367,8 +383,15 @@ class CaseSerializerReduced( ): @staticmethod - def allowed_fields(): - return config.ALLOWED_FIELDS_BLOCKED_CASE.split(",") + def blocked_fields(): + """ + Returns a list of blocked fields for the CaseSerializerReduced. + The blocked fields are defined in the config.BLOCKED_FIELDS_CASE setting. + If the setting is not defined, an empty list is returned. + """ + if not config.BLOCKED_FIELDS_CASE: + return [] + return config.BLOCKED_FIELDS_CASE.split(",") class Meta: model = models.Case diff --git a/ngen/serializers/common/mixins.py b/ngen/serializers/common/mixins.py index d5c27b4a..634b93e3 100644 --- a/ngen/serializers/common/mixins.py +++ b/ngen/serializers/common/mixins.py @@ -112,11 +112,11 @@ def get_extra_kwargs(self): and not self.instance.mergeable ): if self.instance.blocked: - allowed_fields = self.allowed_fields() + blocked_fields = self.blocked_fields() elif self.instance.merged: - allowed_fields = [] + blocked_fields = [] for field in self.instance._meta.fields: - if field.name not in allowed_fields: + if field.name in blocked_fields: kwargs = extra_kwargs.get(field.name, {}) kwargs["read_only"] = True if field.is_relation: diff --git a/ngen/tasks.py b/ngen/tasks.py index c808c99a..1ffba82f 100644 --- a/ngen/tasks.py +++ b/ngen/tasks.py @@ -167,11 +167,9 @@ def enrich_artifact(artifact_id): success=report.report.get("success"), ) if config.ARTIFACT_RECURSIVE_ENRICHMENT: + allowed_types = config.ALLOWED_ARTIFACTS_TYPES or "" for job_artifact in api.jobs.get_artifacts(job.id): - if ( - job_artifact.dataType - in config.ALLOWED_ARTIFACTS_TYPES.split(",") - ): + if job_artifact.dataType in allowed_types.split(","): new_artifact, created = ( ngen.models.Artifact.objects.get_or_create( value=job_artifact.data, @@ -208,12 +206,14 @@ def retest_event_kintun(event_id): """ try: event = ngen.models.Event.objects.get(pk=event_id) - mapping_to = ngen.models.AnalyzerMapping.objects.get( + analyzer_mapping = ngen.models.AnalyzerMapping.objects.get( mapping_from=event.taxonomy - ).mapping_to + ) + mapping_to = analyzer_mapping.mapping_to + analyzer_type = analyzer_mapping.analyzer_type analysis_data = { "date": timezone.now(), - "analyzer_type": "kintun", + "analyzer_type": analyzer_type, "vulnerable": False, "result": "in_progress", "target": event.address_value, @@ -222,22 +222,24 @@ def retest_event_kintun(event_id): "event": event, } event_analysis = ngen.models.EventAnalysis.objects.create(**analysis_data) - + kintun_data = kintun.retest_event_kintun(event, mapping_to) - + event_analysis.vulnerable = kintun_data.get("vulnerable", False) event_analysis.result = kintun_data.get("evidence", "") event_analysis.scan_type = kintun_data.get("vuln_type", "") event_analysis.analyzer_url = kintun_data.get("_id", "") event_analysis.save() - + return kintun_data except Exception as e: try: event_analysis.delete() except Exception as delete_error: - return {"error": f"Original error: {str(e)}, Deletion error: {str(delete_error)}"} + return { + "error": f"Original error: {str(e)}, Deletion error: {str(delete_error)}" + } return {"error": str(e)} diff --git a/ngen/utils.py b/ngen/utils.py index 7141b8b1..d6efbac5 100644 --- a/ngen/utils.py +++ b/ngen/utils.py @@ -61,6 +61,6 @@ def get_mime_type(file): def clean_list(input_list): """ - Remove None values and empty strings from a list + Remove None and empty values from a list """ - return list(filter(None, input_list)) + return [item for item in input_list if item not in (None, "", [], {}, ())] diff --git a/ngen/views/analyzer_mapping.py b/ngen/views/analyzer_mapping.py index 16749257..ff2a8a9b 100644 --- a/ngen/views/analyzer_mapping.py +++ b/ngen/views/analyzer_mapping.py @@ -1,8 +1,16 @@ -from rest_framework import viewsets - +import django_filters +from rest_framework import viewsets, filters, mixins +from ngen.filters import AnalyzerMappingFilter from ngen import models, serializers class AnalyzerMappingViewSet(viewsets.ModelViewSet): queryset = models.AnalyzerMapping.objects.all() + filter_backends = [ + filters.SearchFilter, + django_filters.rest_framework.DjangoFilterBackend, + filters.OrderingFilter, + ] + search_fields = ["mapping_to", "mapping_from__name", "analyzer_type"] + filterset_class = AnalyzerMappingFilter serializer_class = serializers.AnalyzerMappingSerializer \ No newline at end of file diff --git a/ngen/views/tools.py b/ngen/views/tools.py index 955de68b..cbfdffe5 100644 --- a/ngen/views/tools.py +++ b/ngen/views/tools.py @@ -22,18 +22,6 @@ from ngen.tasks import whois_lookup_task -class AboutView(TemplateView): - html = True - template_name = "reports/base.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["html"] = True - context["case"] = models.Case.objects.get(pk=161701) - context["config"] = constance.config - return context - - class DisabledView(APIView): """View for disabled endpoints""" @@ -262,3 +250,28 @@ def get(self, request, task_id): }, status=status.HTTP_500_INTERNAL_SERVER_ERROR, ) + + +class VersionView(APIView): + """ + View to return about the version of the application. + """ + + permission_classes = [permissions.IsAuthenticated] + + def get(self, request): + version = settings.APP_VERSION_TAG + commit = settings.APP_COMMIT + branch = settings.APP_BRANCH + build_file = settings.APP_BUILD_FILE + mode = "development" if settings.DEBUG else "production" + return Response( + { + "version": version, + "commit": commit, + "branch": branch, + "build_file": build_file, + "environment": mode, + }, + status=status.HTTP_200_OK, + ) diff --git a/project/settings.py b/project/settings.py index e7d9512c..9189959d 100644 --- a/project/settings.py +++ b/project/settings.py @@ -12,6 +12,7 @@ import os import shutil +import re from datetime import timedelta from pathlib import Path @@ -279,20 +280,23 @@ gettext_lazy( "CSIRT team email. This is an email to receive notifications and reports from ngen to the team" ), + str, ), "TEAM_EMAIL_PRIORITY": ( os.environ.get("TEAM_EMAIL_PRIORITY"), gettext_lazy( "CSIRT team email default priority (Critical is the lowest and will only recieve critical emails, Very low is the highest and will recieve all emails) priority_field" ), + "priority_field", ), "TEAM_ABUSE": ( os.environ.get("TEAM_ABUSE"), gettext_lazy( "CSIRT abuse email. This is an email to receive abuse reports from external sources" ), + str, ), - "TEAM_URL": (os.environ.get("TEAM_URL"), gettext_lazy("CSIRT site url")), + "TEAM_URL": (os.environ.get("TEAM_URL"), gettext_lazy("CSIRT site url"), str), "TEAM_LOGO": ( os.path.join(CONSTANCE_FILE_ROOT, "teamlogo.png"), gettext_lazy( @@ -305,14 +309,16 @@ gettext_lazy( "Team logo url for emails. Overrides the saved logo. Usefull to access the logo from a public url" ), + str, ), - "TEAM_NAME": (os.environ.get("TEAM_NAME"), "CSIRT name"), - "EMAIL_HOST": (os.environ.get("EMAIL_HOST"), "Email host"), + "TEAM_NAME": (os.environ.get("TEAM_NAME"), "CSIRT name", str), + "EMAIL_HOST": (os.environ.get("EMAIL_HOST"), "Email host", str), "EMAIL_SENDER": ( os.environ.get("EMAIL_SENDER"), gettext_lazy( "SMTP sender email address. This is the email that will be used to send emails from ngen" ), + str, ), "EMAIL_USERNAME": ( os.environ.get("EMAIL_USERNAME"), @@ -322,7 +328,7 @@ os.environ.get("EMAIL_PASSWORD"), "Email password to fetch (required) and send emails (optional)", ), - "EMAIL_PORT": (os.environ.get("EMAIL_PORT"), "Email port to send emails"), + "EMAIL_PORT": (os.environ.get("EMAIL_PORT"), "Email port to send emails", int), "EMAIL_USE_TLS": ( os.environ.get("EMAIL_USE_TLS", "false").lower() in VALUES_TRUE, "Email use TLS to send emails", @@ -338,28 +344,31 @@ gettext_lazy( "Case comma separated fields that could be modified if the instance is merged" ), + list, ), "ALLOWED_FIELDS_MERGED_EVENT": ( os.environ.get("ALLOWED_FIELDS_MERGED_EVENT"), gettext_lazy( "Event comma separated fields that could be modified if the instance is merged" ), + list, ), - "ALLOWED_FIELDS_BLOCKED_CASE": ( - os.environ.get("ALLOWED_FIELDS_BLOCKED_CASE"), + "BLOCKED_FIELDS_CASE": ( + os.environ.get("BLOCKED_FIELDS_CASE"), gettext_lazy( "Case comma separated fields that could be modified if the instance is blocked" ), + list, ), - "ALLOWED_FIELDS_BLOCKED_EVENT": ( - os.environ.get("ALLOWED_FIELDS_BLOCKED_EVENT"), + "BLOCKED_FIELDS_EVENT": ( + os.environ.get("BLOCKED_FIELDS_EVENT"), gettext_lazy( "Event comma separated fields that could be modified if the instance is blocked" ), + list, ), - "ALLOWED_FIELDS_BLOCKED_EXCEPTION": ( - os.environ.get("ALLOWED_FIELDS_BLOCKED_EXCEPTION", "false").lower() - in VALUES_TRUE, + "BLOCKED_FIELDS_EXCEPTION": ( + os.environ.get("BLOCKED_FIELDS_EXCEPTION", "false").lower() in VALUES_TRUE, gettext_lazy( "If True, ngen will raise an exception if a blocked field is modified" ), @@ -408,18 +417,22 @@ "CORTEX_HOST": ( os.environ.get("CORTEX_HOST"), gettext_lazy("Cortex host domain:port"), + str, ), "CORTEX_APIKEY": ( os.environ.get("CORTEX_APIKEY", ""), gettext_lazy("Cortex admin apikey"), + str, ), "KINTUN_HOST": ( os.environ.get("KINTUN_HOST"), gettext_lazy("Kintun host domain:port"), + str, ), "KINTUN_APIKEY": ( os.environ.get("KINTUN_APIKEY", ""), gettext_lazy("Kintun admin apikey"), + str, ), "PAGE_SIZE": ( int(os.environ.get("PAGE_SIZE", 10)), @@ -606,6 +619,76 @@ def show_toolbar(request): } SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +# Get APP Version, Commit and Branch from environment variables +def read_git_file(path): + try: + with open(path, "r") as f: + return f.read().strip() + except Exception: + return None + + +def get_git_info_from_files(): + git_dir = os.path.join(BASE_DIR, ".git") + + # Get the commit + head_path = os.path.join(git_dir, "HEAD") + head_content = read_git_file(head_path) + + if not head_content: + return "unknown", "unknown", "unknown" + + if head_content.startswith("ref:"): + ref = head_content.split(" ")[1].strip() + ref_path = os.path.join(git_dir, ref) + commit = read_git_file(ref_path) + branch = os.path.basename(ref.split("/")[-1]) + else: + commit = head_content + branch = "detached" + + if not commit: + return "unknown", "unknown", "unknown" + + # Search for the latest tag pointing to that commit (in .git/refs/tags or packed-refs) + tags_dir = os.path.join(git_dir, "refs", "tags") + tag = None + + # Search in refs/tags + if os.path.isdir(tags_dir): + for t in sorted(os.listdir(tags_dir), reverse=True): + tag_path = os.path.join(tags_dir, t) + tag_commit = read_git_file(tag_path) + if tag_commit and commit.startswith(tag_commit[: len(commit)]): + tag = t + break + + # If not found, search in packed-refs + if not tag: + packed_refs = os.path.join(git_dir, "packed-refs") + if os.path.exists(packed_refs): + with open(packed_refs, "r") as f: + for line in f: + if re.match(r"^[0-9a-f]{40} refs/tags/", line): + hash_, ref = line.strip().split(" ") + if commit.startswith(hash_[: len(commit)]): + tag = ref.split("/")[-1] + break + + version = tag if tag else "dev" + return version, commit[:7], branch + + +APP_VERSION_TAG = os.environ.get("APP_VERSION_TAG", "") +APP_COMMIT = os.environ.get("APP_COMMIT", "") +APP_BRANCH = os.environ.get("APP_BRANCH", "") +APP_BUILD_FILE = os.environ.get("APP_BUILD_FILE", "unknown") + +if not all([APP_VERSION_TAG, APP_COMMIT, APP_BRANCH]): + APP_VERSION_TAG, APP_COMMIT, APP_BRANCH = get_git_info_from_files() + + # These variables are used to provide visualization on the admin interface # and are not used in the code ENVIRON_CONFIG = { diff --git a/project/urls.py b/project/urls.py index d4aab9a8..44f6a946 100644 --- a/project/urls.py +++ b/project/urls.py @@ -149,7 +149,9 @@ router.register(r"eventanalysis", views.EventAnalysisViewSet, basename="eventanalysis") -router.register(r"analyzermapping", views.AnalyzerMappingViewSet, basename="analyzermapping") +router.register( + r"analyzermapping", views.AnalyzerMappingViewSet, basename="analyzermapping" +) if settings.ELASTIC_ENABLED: from ngen.documents import CaseDocumentViewSet @@ -196,7 +198,6 @@ views.CookieTokenLogoutView.as_view(), name="ctoken-logout", ), - path("api/about/", views.AboutView.as_view(), name="about"), path("__debug__/", include("debug_toolbar.urls")), path("api/schema/", SpectacularAPIView.as_view(), name="schema"), path( @@ -241,6 +242,11 @@ path( "api/result//", views.TaskStatusView.as_view(), name="task_status" ), + path( + "api/version/", + views.VersionView.as_view(), + name="version", + ), ] if not settings.ELASTIC_ENABLED: diff --git a/requirements.in b/requirements.in index a0380960..6515d2fd 100755 --- a/requirements.in +++ b/requirements.in @@ -1,5 +1,5 @@ cortex4py==2.1.0 -django==4.2.18 +django==4.2.20 django-auditlog==2.3.0 django-bleach==3.0.0 django-celery-beat==2.5.0 @@ -45,3 +45,4 @@ whoisit==3.0.4 publicsuffix2==2.20191221 django-taggit==6.1.0 tornado==6.4.2 +imbox==0.9.8 diff --git a/requirements.txt b/requirements.txt index 4a53af9d..d6b4a225 100755 --- a/requirements.txt +++ b/requirements.txt @@ -35,6 +35,8 @@ certifi==2024.7.4 # httpcore # httpx # requests +chardet==5.2.0 + # via imbox charset-normalizer==3.2.0 # via requests click==8.1.6 @@ -56,7 +58,7 @@ cron-descriptor==1.4.0 # via django-celery-beat decorator==5.1.1 # via validators -django==4.2.17 +django==4.2.20 # via # -r requirements.in # django-appconf @@ -168,6 +170,8 @@ idna==3.7 # anyio # httpx # requests +imbox==0.9.8 + # via -r requirements.in inflection==0.5.1 # via drf-spectacular jsonschema==4.18.4 @@ -307,9 +311,6 @@ wheel==0.41.0 whoisit==3.0.4 # via -r requirements.in -imapclient==3.0.1 -imbox - # The following packages are considered to be unsafe in a requirements file: # pip # setuptools