[
  {
    "path": ".github/ISSUE_TEMPLATE/BUG_REPORT.md",
    "content": "---\nname: Bug Report\nlabels: kind/bug\nabout: If something isn't working as expected.\n---\n\n## Description\n\n<!--\nBriefly describe the problem you are having in a few paragraphs.\n-->\n\n## What did you expect to happen?\n\n\n## What happened instead?\n\n\n## Output of run with `POSTEE_DEBUG=true`:\n\n```\n(paste your output here)\n```\n\n## Additional details (environment setup, networking info...):"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/FEATURE_REQUEST.md",
    "content": "---\nname: Feature Request\nlabels: kind/feature\nabout: I have a suggestion (and might want to implement myself)!\n---\n\n# Description\n<!--Describe what your feature would look like\nThe more information you add as part of your abstract,\nthe easier it will be understand-->\n\n# Use Case\n<!--Add use cases for your feature that demonstrate the need for it\nas a template you could use the following while writing a use case:\nAs a User of X,\nI would like to see Y,\nso that I can accomplish Z\n-->\n\n# Acceptance Criteria\n<!--Define what it would mean for this feature to fully marked as done\nand add value to you. An example:\nWhen feature XYZ is implemented, I will be able to do ABC task-->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md",
    "content": "---\nname: Support Question\nlabels: triage/support\nabout: If you have a question about Postee.\n---\n\n<!--\nIf you have a trouble, feel free to ask.\nMake sure you're not asking duplicate question by searching on the issues lists.\n-->"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n- package-ecosystem: github-actions\n  directory: /\n  schedule:\n    interval: daily\n- package-ecosystem: docker\n  directory: /\n  schedule:\n    interval: daily"
  },
  {
    "path": ".github/workflows/aqua-cloud.yml",
    "content": "name: Aqua Cloud\n\non:\n  push:\n    branches: [ main ]\n  schedule:\n    - cron: '15 21 * * 2'\n\njobs:\n  build:\n    name: Vulnerability Scan\n    runs-on: \"ubuntu-24.04\"\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Run Trivy vulnerability scanner against Aqua Cloud\n        uses: simar7/trivy-action@fe9b9e7e3c0d9e764d9c018d5603f57fba6aba3d # refer: https://github.com/actions/runner/issues/2033\n        with:\n          scan-type: 'fs'\n          hide-progress: true\n          format: 'table'\n          scanners: 'vuln,config'\n        env:\n          AQUA_KEY: ${{ secrets.AQUA_KEY }}\n          AQUA_SECRET: ${{ secrets.AQUA_SECRET }}\n          TRIVY_RUN_AS_PLUGIN: 'aqua'\n\n"
  },
  {
    "path": ".github/workflows/go.yml",
    "content": "---\nname: Pull Request\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\nenv:\n  GO_VERSION: \"1.18\"\n\njobs:\n  build:\n    name: Checks\n    runs-on: ubuntu-20.04\n    steps:\n\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: ${{ env.GO_VERSION }}\n        id: go\n\n      - name: Check out code into the Go module directory\n        uses: actions/checkout@v3\n\n      - name: Setup golangci-lint\n        uses: golangci/golangci-lint-action@v3.1.0\n        with:\n          args: --timeout=10m --verbose\n          version: v1.45\n\n      - name: Build\n        run: make build\n\n      - name: Run Unit Tests\n        run: make test\n\n      - name: Run Integration Tests\n        run: make test-integration\n\n      - name: Run Trivy vulnerability scanner in repo mode\n        uses: aquasecurity/trivy-action@0.11.0\n        with:\n          scan-type: 'fs'\n          ignore-unfixed: true\n          format: 'sarif'\n          output: 'trivy-results.sarif'\n          severity: 'CRITICAL'\n          exit-code: 0\n\n      - name: Upload Trivy scan results to GitHub Security tab\n        uses: github/codeql-action/upload-sarif@v2\n        with:\n          sarif_file: 'trivy-results.sarif'\n      \n      - name: Run Trivy vulnerability scanner in IaC mode\n        uses: aquasecurity/trivy-action@0.11.0\n        with:\n          scan-type: 'config'\n          hide-progress: false\n          format: 'table'"
  },
  {
    "path": ".github/workflows/publish-chart.yml",
    "content": "# Triggered manually using as input the release e.g. v0.0.1\nname: Publish Helm Chart\non:\n  pull_request:\n    branches:\n      - main\n    paths:\n      - 'deploy/helm/**'\n      - 'deploy/kubernetes/**'\n  push:\n    tags:\n      - \"v*\"\n  workflow_dispatch: # manually it will get the latest tag to publish the helm chart\nenv:\n  HELM_REP: helm-charts\n  GH_OWNER: aquasecurity\n  CHART_DIR: deploy/helm/postee\n  GO_VERSION: \"1.18\"\n  KIND_VERSION: \"v0.12.0\"\n  KIND_IMAGE: \"kindest/node:v1.23.4@sha256:0e34f0d0fd448aa2f2819cfd74e99fe5793a6e4938b328f657c8e3f81ee0dfb9\"\njobs:\n  publish-chart:\n    if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'\n    runs-on: ubuntu-20.04\n    steps:\n      - name: Checkout\n        uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748\n        with:\n          fetch-depth: 0\n      - name: Install Helm\n        uses: azure/setup-helm@v3.5\n        with:\n          version: v3.6.0\n      - name: Install chart-releaser\n        env:\n          VERSION: 1.3.0\n        run: |\n          wget \"https://github.com/helm/chart-releaser/releases/download/v${VERSION}/chart-releaser_${VERSION}_linux_amd64.tar.gz\"\n          tar xzvf chart-releaser_${VERSION}_linux_amd64.tar.gz cr\n      - name: 'Action Get Latest Tag'\n        uses: 'actions-ecosystem/action-get-latest-tag@v1.6.0'\n        id: 'get-latest-tag'\n        with:\n          semver_only: true\n      - name: 'Determine default bump'\n        id: 'bump'\n        run: |\n          LATEST_TAG=${{ steps.get-latest-tag.outputs.tag }}\n          if [ \"$LATEST_TAG\" = \"v0.0.0\" ]; then\n              echo \"::set-output name=type::major\"\n          else\n              echo \"::set-output name=type::patch\"\n          fi\n      - name: Package helm chart\n        run: |\n          RELEASE=${{ steps.get-latest-tag.outputs.tag }}\n          echo \"Release ${RELEASE}\"\n          helm package --app-version=${RELEASE} --version=${RELEASE} ${{ env.CHART_DIR }} -d .cr-release-packages\n      - name: Upload helm chart\n        # Failed with upload the same version: https://github.com/helm/chart-releaser/issues/101\n        continue-on-error: true\n        ## Upload the tar in the Releases repository\n        run: |\n          ./cr upload -o ${{ env.GH_OWNER }} -r ${{ env.HELM_REP }} --token ${{ secrets.ORG_REPO_TOKEN }} -p .cr-release-packages\n      - name: Index helm chart\n        run: |\n          ./cr index -o ${{ env.GH_OWNER }} -r ${{ env.HELM_REP }} -c https://${{ env.GH_OWNER }}.github.io/${{ env.HELM_REP }}/ -i index.yaml\n\n      - name: Push index file\n        uses: dmnemec/copy_file_to_another_repo_action@v1.0.4\n        env:\n          API_TOKEN_GITHUB: ${{ secrets.ORG_REPO_TOKEN }}\n        with:\n          source_file: 'index.yaml'\n          destination_repo: '${{ env.GH_OWNER }}/${{ env.HELM_REP }}'\n          destination_folder: '.'\n          destination_branch: 'gh-pages'\n          user_email: aqua-bot@users.noreply.github.com\n          user_name: 'aqua-bot'\n"
  },
  {
    "path": ".github/workflows/publish-docs.yml",
    "content": "---\n# This is a manually triggered workflow to build and publish the MkDocs from the\n# specified Git revision to GitHub pages on https://aquasecurity.github.io/postee\nname: Publish Documentation\n\non:\n  workflow_dispatch:\n    inputs:\n      ref:\n        description: The branch, tag or SHA to deploy, e.g. v0.0.1\n        required: true\n\n# Disable permissions granted to the GITHUB_TOKEN for all the available scopes.\npermissions: {}\n\njobs:\n  deploy:\n    name: Deploy documentation\n    runs-on: ubuntu-20.04\n    permissions:\n      contents: write\n    steps:\n    - name: Checkout main\n      uses: actions/checkout@v3\n      with:\n        ref: ${{ github.event.inputs.ref }}\n        fetch-depth: 0\n        persist-credentials: true\n    - uses: actions/setup-python@v4\n      with:\n        python-version: 3.x\n    - run: |\n        pip install git+https://${GH_TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git\n        pip install mike\n        pip install mkdocs-macros-plugin\n      env:\n        # Note: It is not the same as ${{ secrets.GITHUB_TOKEN }} !\n        GH_TOKEN: ${{ secrets.MKDOCS_AQUA_BOT }}\n    - run: |\n        git config user.name \"aqua-bot\"\n        git config user.email \"aqua-bot@users.noreply.github.com\"\n    - run: |\n        mike deploy --push --update-aliases ${{ github.event.inputs.ref }} latest"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: Release\non:\n  push:\n    tags:\n      - \"*\"\n  workflow_dispatch:\nenv:\n  GO_VERSION: \"1.18\"\njobs:\n  tests:\n    name: Run Tests\n    runs-on: ubuntu-20.04\n    steps:\n      - name: Setup Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Checkout code\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n\n      - name: Build\n        run: make build\n\n      - name: Run Unit tests\n        run: |\n          make test\n\n      - name: Run Integration Tests\n        run: make test-integration\n\n      - name: Run Trivy vulnerability scanner in repo mode\n        uses: aquasecurity/trivy-action@0.11.0\n        with:\n          scan-type: 'fs'\n          ignore-unfixed: true\n          format: 'sarif'\n          severity: 'CRITICAL'\n          exit-code: 0\n\n      - name: Run Trivy vulnerability scanner against Aqua Cloud\n        uses: aquasecurity/trivy-action@0.11.0\n        with:\n          scan-type: 'fs'\n          hide-progress: true\n          format: 'table'\n          security-checks: 'vuln,config'\n        env:\n          AQUA_KEY: ${{ secrets.AQUA_KEY }}\n          AQUA_SECRET: ${{ secrets.AQUA_SECRET }}\n          TRIVY_RUN_AS_PLUGIN: 'aqua'\n\n  release:\n    name: Release\n    needs:\n      - tests\n    runs-on: ubuntu-20.04\n    steps:\n      - name: Setup Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: ${{ env.GO_VERSION }}\n\n      - name: Checkout code\n        uses: actions/checkout@v3\n        with:\n          fetch-depth: 0\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v2\n        with:\n          username: ${{ secrets.DOCKERHUB_USER }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Login to ECR\n        uses: docker/login-action@v2\n        with:\n          registry: public.ecr.aws\n          username: ${{ secrets.ECR_ACCESS_KEY_ID }}\n          password: ${{ secrets.ECR_SECRET_ACCESS_KEY }}\n\n      - name: Release\n        uses: goreleaser/goreleaser-action@v4\n        with:\n          version: ~> 0.180\n          args: release --rm-dist\n          workdir: .\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n      - name: Log out from docker.io and ECR registries\n        if: ${{ always() }}\n        run: |\n          docker logout docker.io\n          docker logout public.ecr.aws\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\nbin/\npkg/\nsrc/github.com/\nsrc/gopkg.in/\nsrc/go.etcd.io/\n**/*.out\n**/*.db\ncoverage.txt\ndist/\n.vscode/\n\n\n"
  },
  {
    "path": ".golangci.yml",
    "content": "run:\n  timeout: 5m\nlinters:\n  enable:\n    - errorlint\n    - govet\n  disable:\n    - gosimple\n    - ineffassign\n    - staticcheck\n\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "project_name: postee\nrelease:\n  draft: false\n  prerelease: auto\nenv:\n  - GO111MODULE=on\n  - CGO_ENABLED=0\nbefore:\n  hooks:\n    - make build\nbuilds:\n  - id: postee\n    dir: .\n    main: ./main.go\n    binary: postee\n    ldflags:\n      - -s -w\n      - \"-extldflags '-static'\"\n      - -X main.version={{.Version}}\n    goos:\n      - darwin\n      - linux\n    goarch:\n      - amd64\n      - arm\n      - arm64\n    goarm:\n      - 7\n    ignore:\n      - goos: darwin\n        goarch: 386      \narchives:\n  - name_template: \"{{ .ProjectName }}_{{.Version}}_{{ .Os }}_{{ .Arch }}\"\n    builds:\n      - postee\n    replacements:\n      amd64: 64bit\n      arm: ARM\n      arm64: ARM64\n      darwin: macOS\n      linux: Linux\nchecksum:\n  name_template: \"checksums.txt\"\nsnapshot:\n  name_template: \"{{ .FullCommit }}\"\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - '^docs'\n      - '^test'\n      - '^release'\ndockers:\n  - dockerfile: Dockerfile.release\n    use: buildx\n    goos: linux\n    goarch: amd64\n    image_templates:\n      - \"docker.io/aquasec/postee:{{ .Version }}-amd64\"\n      - \"public.ecr.aws/aquasecurity/postee:{{ .Version }}-amd64\"\n      - \"docker.io/aquasec/postee:latest\"\n      - \"public.ecr.aws/aquasecurity/postee:latest\"\n    ids:\n      - postee\n    extra_files:\n      - rego-templates/\n      - rego-filters/\n      - cfg.yaml\n    build_flag_templates:\n      - \"--label=org.opencontainers.image.title={{ .ProjectName }}\"\n      - \"--label=org.opencontainers.image.description=Command line interface for Postee\"\n      - \"--label=org.opencontainers.image.vendor=Aqua Security\"\n      - \"--label=org.opencontainers.image.version={{ .Version }}\"\n      - \"--label=org.opencontainers.image.created={{ .Date }}\"\n      - \"--label=org.opencontainers.image.source=https://github.com/aquasecurity/postee\"\n      - \"--label=org.opencontainers.image.revision={{ .FullCommit }}\"\n      - \"--platform=linux/amd64\"\n  - dockerfile: Dockerfile.ui\n    use: buildx\n    goos: linux\n    goarch: amd64\n    image_templates:\n      - \"docker.io/aquasec/postee-ui:{{ .Version }}-amd64\"\n      - \"public.ecr.aws/aquasecurity/postee-ui:{{ .Version }}-amd64\"\n      - \"docker.io/aquasec/postee-ui:latest\"\n      - \"public.ecr.aws/aquasecurity/postee-ui:latest\"\n    ids:\n      - postee-ui\n    extra_files:\n      - rego-templates/\n      - rego-filters/\n      - cfg.yaml\n      - ui/\n    build_flag_templates:\n      - \"--label=org.opencontainers.image.title={{ .ProjectName }}\"\n      - \"--label=org.opencontainers.image.description=Postee UI\"\n      - \"--label=org.opencontainers.image.vendor=Aqua Security\"\n      - \"--label=org.opencontainers.image.version={{ .Version }}\"\n      - \"--label=org.opencontainers.image.created={{ .Date }}\"\n      - \"--label=org.opencontainers.image.source=https://github.com/aquasecurity/postee\"\n      - \"--label=org.opencontainers.image.revision={{ .FullCommit }}\"\n      - \"--label=org.opencontainers.image.documentation=https://aquasecurity.github.io/postee/v{{ .Version }}/\"\n      - \"--platform=linux/amd64\"\ndocker_manifests:\n  - name_template: 'aquasec/postee:{{ .Version }}'\n    image_templates:\n    - 'aquasec/postee:{{ .Version }}-amd64'\n  - name_template: 'public.ecr.aws/aquasecurity/postee:{{ .Version }}'\n    image_templates:\n    - 'public.ecr.aws/aquasecurity/postee:{{ .Version }}-amd64'\n  - name_template: 'aquasec/postee:latest'\n    image_templates:\n    - 'aquasec/postee:{{ .Version }}-amd64'\n# Postee-UI\n  - name_template: 'aquasec/postee-ui:{{ .Version }}'\n    image_templates:\n    - 'aquasec/postee-ui:{{ .Version }}-amd64'\n  - name_template: 'public.ecr.aws/aquasecurity/postee-ui:{{ .Version }}'\n    image_templates:\n    - 'public.ecr.aws/aquasecurity/postee-ui:{{ .Version }}-amd64'\n  - name_template: 'aquasec/postee-ui:latest'\n    image_templates:\n    - 'aquasec/postee-ui:{{ .Version }}-amd64'\n"
  },
  {
    "path": ".yamllint",
    "content": "---\nextends: default\n\nrules:\n  line-length: disable\n  truthy: disable\n  document-start: disable\n\nignore: |\n  /src/\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.18-alpine as builder\n# RUN apk add --update git\nCOPY . /server/\nWORKDIR /server/\nARG TARGETOS TARGETARCH\nRUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build --ldflags \"-s -w\" -o ./bin/postee main.go\n\nFROM alpine:3.18.2\nRUN apk update && apk add wget ca-certificates curl jq\nEXPOSE 8082\nEXPOSE 8445\nRUN mkdir /server\nRUN mkdir /server/database\nRUN mkdir /config\n\nCOPY --from=builder /server/bin /server/\nCOPY --from=builder /server/rego-templates /server/rego-templates\nCOPY --from=builder /server/rego-filters /server/rego-filters\nCOPY --from=builder /server/cfg.yaml /server/cfg.yaml\nWORKDIR /server\nRUN chmod +x postee\nRUN addgroup -g 1099 postee\nRUN adduser -D -g '' -G postee -u 1099 postee\nRUN chown -R postee:postee /server\nRUN chown -R postee:postee /config\nUSER postee\nENTRYPOINT [\"/server/postee\"]\n"
  },
  {
    "path": "Dockerfile.release",
    "content": "FROM alpine:3.18.2\nRUN apk add --no-cache \\\n  ca-certificates \\\n  curl \\\n  jq \\\n  wget\nEXPOSE 8082\nEXPOSE 8445\nRUN mkdir /server\nRUN mkdir /server/database\nRUN mkdir /config\nCOPY postee /server/\nCOPY rego-templates /server/rego-templates\nCOPY rego-filters /server/rego-filters\nCOPY cfg.yaml /config/\nWORKDIR /server\nRUN chmod +x postee\nRUN addgroup -g 1099 postee\nRUN adduser -D -g '' -G postee -u 1099 postee\nRUN chown -R postee:postee /server\nRUN chown -R postee:postee /config\nUSER postee\nENTRYPOINT [\"/server/postee\"]\n"
  },
  {
    "path": "Dockerfile.ui",
    "content": "FROM node:18-alpine3.17 as vuebuilder\nCOPY ./ui/frontend /frontend\nWORKDIR /frontend\n\nRUN yarn install\nRUN yarn build\n\n\nFROM golang:1.18-alpine as gobuilder\n\nCOPY . /server\nWORKDIR /server/ui/backend\nRUN apk add git\nARG TARGETOS TARGETARCH\nRUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build --ldflags \"-s -w\" -o posteeui\n\nFROM alpine:3.18.2\nEXPOSE 8001\n\nRUN mkdir /uiserver\nRUN mkdir /uiserver/www\n\nRUN mkdir /server\nRUN mkdir /server/database\nRUN mkdir /config\n\nCOPY --from=gobuilder /server/ui/backend/posteeui /uiserver\nCOPY --from=vuebuilder /frontend/dist /uiserver/www\n\nWORKDIR /uiserver\nRUN addgroup -g 1099 postee\nRUN adduser -D -g '' -G postee -u 1099 postee\nRUN chown -R postee:postee /server\nRUN chown -R postee:postee /config\nRUN chown -R postee:postee /uiserver\nUSER postee\nENTRYPOINT [\"/uiserver/posteeui\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2016 \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "# Set the default goal\n.DEFAULT_GOAL := build\nVERSION := $(shell git describe --tags)\nLDFLAGS=-ldflags \"-s -w -X=main.version=$(VERSION)\"\n\n# Active module mode, as we use Go modules to manage dependencies\nexport GO111MODULE=on\n\nGO_FMT=gofmt\n\n.PHONY: build fmt vet test\n\ndefault : build\n\n.PHONY: build\nbuild :\n\t@echo \"Building Postee....\"\n\tCGO_ENABLED=0 go build $(LDFLAGS) -o ./postee main.go\n\t@echo \"Done!\"\n\nfmt :\n\t@echo \"fmt....\"\n\t$(GO_FMT) -s -w ./\n\ntest :\n\tgo test -race -v -timeout=30s ./...\n\ntest-integration:\n\tgo test -race -v -tags=integration -timeout=30s ./...\n\ncover :\n\tgo test ./msgservice ./dbservice ./router ./formatting ./data ./regoservice ./routes ./actions -v -coverprofile=cover.out\n\tgo tool cover -html=cover.out\n\ncomposer :\n\t@echo \"Running Postee UI....\"\n\tdocker-compose up --build\n\ndocker-webhook : build\n\t@echo \"Building image Dockerfile.release....\"\n\tdocker build --no-cache -t aquasec/postee:latest -f Dockerfile.release .\n\tdocker run -p 8082:8082 -p 8445:8445 aquasec/postee:latest --cfgfile /server/cfg.yaml\n\ndocker-ui :\n\t@echo \"Building image Dockerfile.ui....\"\n\tdocker build --no-cache -t aquasec/postee-ui:latest -f Dockerfile.ui .\n\ndeploy-k8s :\n\t@echo \"Deploy Postee in Kubernetes....\"\n\tkubectl create -f deploy/kubernetes\n\tkubectl wait --for=condition=available \\\n          --timeout=1m deploy/postee\n"
  },
  {
    "path": "README.md",
    "content": "# Notice: Postee is no longer under active development or maintenance.\n\n<p align=\"center\">\n  <img src=\"./docs/img/postee.png\">\n</p>\n\n![Docker Pulls][docker-pull]\n[![Go Report Card][report-card-img]][report-card]\n![](https://github.com/aquasecurity/postee/workflows/Go/badge.svg)\n[![License][license-img]][license]\n<a href=\"https://slack.aquasec.com/?_ga=2.51428586.2119512742.1655808394-1739877964.1641199050\">\n<img src=\"https://img.shields.io/static/v1?label=Slack&message=Join+our+Community&color=4a154b&logo=slack\">\n</a>\n\n\n[download]: https://img.shields.io/github/downloads/aquasecurity/postee/total?logo=github\n[release-img]: https://img.shields.io/github/release/aquasecurity/postee.png?logo=github\n[release]: https://github.com/aquasecurity/postee/releases\n[docker-pull]: https://img.shields.io/docker/pulls/aquasec/postee?logo=docker&label=docker%20pulls%20%2F%20postee\n[go-doc-img]: https://godoc.org/github.com/aquasecurity/postee?status.svg\n[report-card-img]: https://goreportcard.com/badge/github.com/aquasecurity/postee\n[report-card]: https://goreportcard.com/report/github.com/aquasecurity/postee\n[license-img]: https://img.shields.io/badge/License-mit-blue.svg\n[license]: https://github.com/aquasecurity/postee/blob/master/LICENSE\n\n\nPostee is a simple message routing application that receives input messages through a webhook interface, and can take enforce actions using predefined outputs via integrations.\n\nWatch a quick demo of how you can use Postee:\n\n\n[![Postee Demo Video](./docs/img/postee-video-thumbnail.jpg)](https://www.youtube.com/watch?v=HZ5Z8jAVH8w)\n\nPrimary use of Postee is to act as a message relay and notification service that integrates with a variety of third-party services. Postee can also be used for sending vulnerability scan results or audit alerts from Aqua Platform to collaboration systems.\n\nIn addition, Postee can also be used to enforce pre-defined behaviours that can orchestrate actions based on input messages as triggers.\n\n![Postee v2 scheme](docs/img/postee-v2-scheme.png)\n\n## Status\nAlthough we are trying to keep new releases backward compatible with previous versions, this project is still incubating,\nand some APIs and code structures may change.\n\n## Documentation\nThe official [Documentation] provides detailed installation, configuration, troubleshooting, and quick start guides.\n\n---\nPostee is an [Aqua Security](https://aquasec.com) open source project.  \nLearn about our [Open Source Work and Portfolio].  \nJoin the community, and talk to us about any matter in [GitHub Discussions] or [Slack].\n\n[Documentation]: https://aquasecurity.github.io/postee/latest\n[Open Source Work and Portfolio]: https://www.aquasec.com/products/open-source-projects/\n[Slack]: https://slack.aquasec.com/\n[GitHub Discussions]: https://github.com/aquasecurity/postee/discussions\n\n\n## Release\n\n1. Bump version of [helm chart](https://github.com/aquasecurity/postee/blob/main/deploy/helm/postee/Chart.yaml).\n1. (By repository admin) Create a new tag. Postee and helm charts are automatically released by github actions.\n1. (By repository admin) Run [publish-docs workflow](https://github.com/aquasecurity/postee/blob/main/.github/workflows/publish-docs.yml), if document has been updated.\n"
  },
  {
    "path": "actions/aws_securityhub.go",
    "content": "package actions\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/config\"\n\t\"github.com/aws/aws-sdk-go-v2/service/securityhub\"\n\t\"github.com/aws/aws-sdk-go-v2/service/securityhub/types\"\n)\n\ntype securityHubAPI interface {\n\tBatchImportFindings(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error)\n}\n\ntype Finding struct {\n\tSchemaVersion string   `json:\"SchemaVersion,omitempty\"`\n\tID            string   `json:\"Id,omitempty\"`\n\tProductArn    string   `json:\"ProductArn,omitempty\"`\n\tGeneratorID   string   `json:\"GeneratorId,omitempty\"`\n\tAwsAccountID  string   `json:\"AwsAccountId,omitempty\"`\n\tTypes         []string `json:\"Types,omitempty\"`\n\tCreatedAt     string   `json:\"CreatedAt,omitempty\"`\n\tUpdatedAt     string   `json:\"UpdatedAt,omitempty\"`\n\tSeverity      struct {\n\t\tLabel string `json:\"Label,omitempty\"`\n\t} `json:\"Severity,omitempty\"`\n\tTitle       string `json:\"Title,omitempty\"`\n\tDescription string `json:\"Description,omitempty\"`\n\tRemediation struct {\n\t\tRecommendation struct {\n\t\t\tText string `json:\"Text,omitempty\"`\n\t\t\tURL  string `json:\"Url,omitempty\"`\n\t\t} `json:\"Recommendation,omitempty\"`\n\t} `json:\"Remediation,omitempty\"`\n\tProductFields struct {\n\t\tProductName string `json:\"Product Name,omitempty\"`\n\t} `json:\"ProductFields,omitempty\"`\n\tResources []struct {\n\t\tType      string `json:\"Type,omitempty\"`\n\t\tID        string `json:\"Id,omitempty\"`\n\t\tPartition string `json:\"Partition,omitempty\"`\n\t\tRegion    string `json:\"Region,omitempty\"`\n\t\tDetails   struct {\n\t\t\tContainer struct {\n\t\t\t\tImageName string `json:\"ImageName,omitempty\"`\n\t\t\t} `json:\"Container,omitempty\"`\n\t\t\tOther struct {\n\t\t\t\tCVEID            string `json:\"CVE ID,omitempty\"`\n\t\t\t\tCVETitle         string `json:\"CVE Title,omitempty\"`\n\t\t\t\tPkgName          string `json:\"PkgName,omitempty\"`\n\t\t\t\tInstalledPackage string `json:\"Installed Package,omitempty\"`\n\t\t\t\tPatchedPackage   string `json:\"Patched Package,omitempty\"`\n\t\t\t\tNvdCvssScoreV3   string `json:\"NvdCvssScoreV3,omitempty\"`\n\t\t\t\tNvdCvssVectorV3  string `json:\"NvdCvssVectorV3,omitempty\"`\n\t\t\t\tNvdCvssScoreV2   string `json:\"NvdCvssScoreV2,omitempty\"`\n\t\t\t\tNvdCvssVectorV2  string `json:\"NvdCvssVectorV2,omitempty\"`\n\t\t\t} `json:\"Other,omitempty\"`\n\t\t} `json:\"Details,omitempty\"`\n\t} `json:\"Resources,omitempty\"`\n\tRecordState string `json:\"RecordState,omitempty\"`\n}\n\ntype Report struct {\n\tFindings []Finding\n}\n\ntype AWSSecurityHubClient struct {\n\tclient securityHubAPI\n\n\tName string\n}\n\nfunc (sh AWSSecurityHubClient) GetName() string {\n\treturn sh.Name\n}\n\nfunc (sh *AWSSecurityHubClient) Init() error {\n\t// Load the Shared AWS Configuration (~/.aws/config)\n\tcfg, err := config.LoadDefaultConfig(context.TODO())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load AWS config: %w\", err)\n\t}\n\n\tsh.client = securityhub.NewFromConfig(cfg)\n\tif sh.client == nil {\n\t\treturn fmt.Errorf(\"failed to create AWS Security Hub client\")\n\t}\n\n\treturn nil\n}\n\nfunc (sh AWSSecurityHubClient) Send(m map[string]string) error {\n\tvar r Report\n\tif err := json.Unmarshal([]byte(m[\"description\"]), &r); err != nil {\n\t\treturn fmt.Errorf(\"AWS Security Hub unmarshalling failed: %w\", err)\n\t}\n\n\tif len(r.Findings) <= 0 {\n\t\treturn fmt.Errorf(\"trivy AWS sent no findings to Postee, skipping sending\")\n\t}\n\n\tvar awsfindings []types.AwsSecurityFinding\n\tfor _, f := range r.Findings {\n\t\taf := types.AwsSecurityFinding{\n\t\t\tAwsAccountId:  aws.String(f.AwsAccountID),\n\t\t\tCreatedAt:     aws.String(f.CreatedAt),\n\t\t\tDescription:   aws.String(f.Description),\n\t\t\tGeneratorId:   aws.String(f.GeneratorID),\n\t\t\tId:            aws.String(f.ID),\n\t\t\tProductArn:    aws.String(f.ProductArn),\n\t\t\tSchemaVersion: aws.String(f.SchemaVersion),\n\t\t\tTitle:         aws.String(f.Title),\n\t\t\tUpdatedAt:     aws.String(f.UpdatedAt),\n\t\t\tTypes:         f.Types,\n\t\t}\n\n\t\taf.Resources = append(af.Resources, []types.Resource{\n\t\t\t{\n\t\t\t\tId:   aws.String(f.ID),\n\t\t\t\tType: aws.String(strings.Join(f.Types, \" \")),\n\t\t\t},\n\t\t}...)\n\n\t\taf.Remediation = &types.Remediation{\n\t\t\tRecommendation: &types.Recommendation{\n\t\t\t\tText: aws.String(f.Remediation.Recommendation.Text),\n\t\t\t\tUrl:  aws.String(f.Remediation.Recommendation.URL),\n\t\t\t},\n\t\t}\n\n\t\taf.Severity = &types.Severity{\n\t\t\tLabel: types.SeverityLabel(f.Severity.Label),\n\t\t}\n\n\t\tawsfindings = append(awsfindings, af)\n\t}\n\n\tvar successCount, failedCount int\n\tawsFindingChunks := chunkBy(awsfindings, 100)\n\tlog.Printf(\"sending %d findings in %d chunk(s) to AWS Security Hub\", len(awsfindings), len(awsFindingChunks))\n\tfor _, awsfindingChunk := range awsFindingChunks {\n\t\toutput, err := sh.client.BatchImportFindings(context.TODO(), &securityhub.BatchImportFindingsInput{\n\t\t\tFindings: awsfindingChunk,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"upload to AWS Security Hub failed: %w\", err)\n\t\t}\n\n\t\tif len(output.FailedFindings) > 0 {\n\t\t\tfailedCount += len(output.FailedFindings)\n\t\t\tlog.Printf(\"%d findings failed to be reported...\", len(output.FailedFindings))\n\t\t\tfor _, ff := range output.FailedFindings {\n\t\t\t\tlog.Printf(\"Failed finding details: ID: %s , ErrorCode: %s, ErrorMessage: %s\\n\", *ff.Id, *ff.ErrorCode, *ff.ErrorMessage)\n\t\t\t}\n\t\t}\n\t\tsuccessCount += int(output.SuccessCount)\n\t}\n\n\tlog.Printf(\"successfully sent: %d findings to AWS Security Hub\", successCount)\n\treturn nil\n}\n\nfunc (sh AWSSecurityHubClient) Terminate() error {\n\treturn nil\n}\n\nfunc (sh AWSSecurityHubClient) GetLayoutProvider() layout.LayoutProvider {\n\t// Todo: This is MOCK. Because Formatting isn't need for Webhook\n\t// todo: The App should work with `return nil`\n\treturn new(formatting.HtmlProvider)\n}\n\nfunc chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {\n\tfor chunkSize < len(items) {\n\t\titems, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize])\n\t}\n\treturn append(chunks, items)\n}\n"
  },
  {
    "path": "actions/aws_securityhub_test.go",
    "content": "package actions\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-v2/service/securityhub\"\n\t\"github.com/aws/aws-sdk-go-v2/service/securityhub/types\"\n\t\"github.com/aws/smithy-go/middleware\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst GoodFindings = `{\n \"Findings\": [\n   {\n     \"SchemaVersion\": \"2018-10-08\",\n     \"Id\": \"alpine:3.10 (alpine 3.10.9)/CVE-2021-36159\",\n     \"ProductArn\": \"arn:aws:securityhub:eu-west-2::product/aquasecurity/aquasecurity\",\n     \"GeneratorId\": \"Trivy/CVE-2021-36159\",\n     \"AwsAccountId\": \"000000\",\n     \"Types\": [\n       \"Software and Configuration Checks/Vulnerabilities/CVE\"\n     ],\n     \"CreatedAt\": \"2022-08-05T22:29:18.549914-07:00\",\n     \"UpdatedAt\": \"2022-08-10T22:29:18.549938-07:00\",\n     \"Severity\": {\n       \"Label\": \"CRITICAL\"\n     },\n     \"Title\": \"Trivy found a vulnerability to CVE-2021-36159 in container alpine:3.10 (alpine 3.10.9)\",\n     \"Description\": \"libfetch before 2021-07-26, as used in apk-tools, xbps, and other products, mishandles numeric strings for the FTP and HTTP protocols. The FTP passive mode implementation allows an out-of-bounds read because strtol is used to parse the relevant numbers into address bytes. It does not check if the line ends prematurely. If it does, the for-loop condition checks for the &#39;\\\\0&#39; terminator one byte too late.\",\n     \"Remediation\": {\n       \"Recommendation\": {\n         \"Text\": \"More information on this vulnerability is provided in the hyperlink\",\n         \"Url\": \"https://avd.aquasec.com/nvd/cve-2021-36159\"\n       }\n     },\n     \"ProductFields\": {\n       \"Product Name\": \"Trivy\"\n     },\n     \"Resources\": [\n       {\n         \"Type\": \"Container\",\n         \"Id\": \"alpine:3.10 (alpine 3.10.9)\",\n         \"Partition\": \"aws\",\n         \"Region\": \"\",\n         \"Details\": {\n           \"Container\": {\n             \"ImageName\": \"alpine:3.10 (alpine 3.10.9)\"\n           },\n           \"Other\": {\n             \"CVE ID\": \"CVE-2021-36159\",\n             \"CVE Title\": \"\",\n             \"PkgName\": \"apk-tools\",\n             \"Installed Package\": \"2.10.6-r0\",\n             \"Patched Package\": \"2.10.7-r0\",\n             \"NvdCvssScoreV3\": \"9.1\",\n             \"NvdCvssVectorV3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H\",\n             \"NvdCvssScoreV2\": \"6.4\",\n             \"NvdCvssVectorV2\": \"AV:N/AC:L/Au:N/C:P/I:N/A:P\"\n           }\n         }\n       }\n     ],\n     \"RecordState\": \"ACTIVE\"\n   },\n   {\n     \"SchemaVersion\": \"2018-10-08\",\n     \"Id\": \"alpine:3.10 (alpine 3.10.9)/CVE-2021-36159\",\n     \"ProductArn\": \"arn:aws:securityhub:eu-west-2::product/aquasecurity/aquasecurity\",\n     \"GeneratorId\": \"Trivy/CVE-2021-36159\",\n     \"AwsAccountId\": \"000000\",\n     \"Types\": [\n       \"Software and Configuration Checks/Vulnerabilities/CVE\"\n     ],\n     \"CreatedAt\": \"2022-08-05T22:29:18.549914-07:00\",\n     \"UpdatedAt\": \"2022-08-10T22:29:18.549938-07:00\",\n     \"Severity\": {\n       \"Label\": \"CRITICAL\"\n     },\n     \"Title\": \"Trivy found a vulnerability to CVE-2021-36159 in container alpine:3.10 (alpine 3.10.9)\",\n     \"Description\": \"libfetch before 2021-07-26, as used in apk-tools, xbps, and other products, mishandles numeric strings for the FTP and HTTP protocols. The FTP passive mode implementation allows an out-of-bounds read because strtol is used to parse the relevant numbers into address bytes. It does not check if the line ends prematurely. If it does, the for-loop condition checks for the &#39;\\\\0&#39; terminator one byte too late.\",\n     \"Remediation\": {\n       \"Recommendation\": {\n         \"Text\": \"More information on this vulnerability is provided in the hyperlink\",\n         \"Url\": \"https://avd.aquasec.com/nvd/cve-2021-36159\"\n       }\n     },\n     \"ProductFields\": {\n       \"Product Name\": \"Trivy\"\n     },\n     \"Resources\": [\n       {\n         \"Type\": \"Container\",\n         \"Id\": \"alpine:3.10 (alpine 3.10.9)\",\n         \"Partition\": \"aws\",\n         \"Region\": \"\",\n         \"Details\": {\n           \"Container\": {\n             \"ImageName\": \"alpine:3.10 (alpine 3.10.9)\"\n           },\n           \"Other\": {\n             \"CVE ID\": \"CVE-2021-36159\",\n             \"CVE Title\": \"\",\n             \"PkgName\": \"apk-tools\",\n             \"Installed Package\": \"2.10.6-r0\",\n             \"Patched Package\": \"2.10.7-r0\",\n             \"NvdCvssScoreV3\": \"9.1\",\n             \"NvdCvssVectorV3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H\",\n             \"NvdCvssScoreV2\": \"6.4\",\n             \"NvdCvssVectorV2\": \"AV:N/AC:L/Au:N/C:P/I:N/A:P\"\n           }\n         }\n       }\n     ],\n     \"RecordState\": \"ACTIVE\"\n   }\n ]\n}`\n\ntype mockAWSSHClient struct {\n\tsecurityHubAPI\n\n\tbatchImportFindingsFunc func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error)\n}\n\nfunc (mc mockAWSSHClient) BatchImportFindings(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) {\n\tif mc.batchImportFindingsFunc != nil {\n\t\treturn mc.batchImportFindingsFunc(ctx, params, optFns...)\n\t}\n\treturn &securityhub.BatchImportFindingsOutput{}, nil\n}\n\nfunc TestAWSSecurityHubClient_Send(t *testing.T) {\n\tt.Run(\"happy path, multiple findings\", func(t *testing.T) {\n\t\tac := AWSSecurityHubClient{\n\t\t\tclient: &mockAWSSHClient{\n\t\t\t\tbatchImportFindingsFunc: func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\tassert.Equal(t, 2, len(params.Findings))\n\n\t\t\t\t\treturn &securityhub.BatchImportFindingsOutput{\n\t\t\t\t\t\tSuccessCount: 2,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\trequire.NoError(t, ac.Send(map[string]string{\n\t\t\t\"description\": GoodFindings,\n\t\t}), t.Name())\n\t})\n\n\tt.Run(\"happy path, no findings\", func(t *testing.T) {\n\t\tac := AWSSecurityHubClient{\n\t\t\tclient: &mockAWSSHClient{\n\t\t\t\tbatchImportFindingsFunc: func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\tassert.Fail(t, \"this method should not have been called\")\n\t\t\t\t\treturn nil, nil\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\trequire.Equal(t, \"trivy AWS sent no findings to Postee, skipping sending\", ac.Send(map[string]string{\n\t\t\t\"description\": `{\"Findings\":[]}`,\n\t\t}).Error(), t.Name())\n\t})\n\n\tt.Run(\"sad path, bad incoming event from trivy\", func(t *testing.T) {\n\t\trequire.Equal(t, \"AWS Security Hub unmarshalling failed: invalid character 'i' looking for beginning of value\", AWSSecurityHubClient{}.Send(map[string]string{\n\t\t\t\"description\": \"invalid json\",\n\t\t}).Error())\n\t})\n\n\tt.Run(\"sad path, aws security hub fails has an error\", func(t *testing.T) {\n\t\tac := AWSSecurityHubClient{\n\t\t\tclient: &mockAWSSHClient{\n\t\t\t\tbatchImportFindingsFunc: func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\treturn &securityhub.BatchImportFindingsOutput{}, fmt.Errorf(\"internal server error\")\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\trequire.Equal(t, \"upload to AWS Security Hub failed: internal server error\", ac.Send(map[string]string{\n\t\t\t\"description\": GoodFindings,\n\t\t}).Error(), t.Name())\n\t})\n\n\tt.Run(\"sad path, aws security hub fails to ingest some findings\", func(t *testing.T) {\n\t\tac := AWSSecurityHubClient{\n\t\t\tclient: &mockAWSSHClient{\n\t\t\t\tbatchImportFindingsFunc: func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) {\n\t\t\t\t\tt.Helper()\n\t\t\t\t\treturn &securityhub.BatchImportFindingsOutput{\n\t\t\t\t\t\tFailedCount:  1,\n\t\t\t\t\t\tSuccessCount: 1,\n\t\t\t\t\t\tFailedFindings: []types.ImportFindingsError{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tErrorCode:    aws.String(\"123\"),\n\t\t\t\t\t\t\t\tErrorMessage: aws.String(\"bad bad\"),\n\t\t\t\t\t\t\t\tId:           aws.String(\"001\"),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tResultMetadata: middleware.Metadata{},\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\trequire.NoError(t, ac.Send(map[string]string{\n\t\t\t\"description\": GoodFindings,\n\t\t}), t.Name())\n\t})\n}\n"
  },
  {
    "path": "actions/dependencytrack.go",
    "content": "package actions\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\tdtrack \"github.com/DependencyTrack/client-go\"\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\ntype DependencyTrackAction struct {\n\tName   string\n\tUrl    string\n\tAPIKey string\n}\n\nfunc (dta *DependencyTrackAction) GetName() string {\n\treturn dta.Name\n}\n\nfunc (dta *DependencyTrackAction) Init() error {\n\tlog.Printf(\"Starting Dependency Track action %s, for sending to %s\", dta.Name, dta.Url)\n\treturn nil\n}\n\nfunc (dta *DependencyTrackAction) Send(content map[string]string) error {\n\tproject, ok := content[\"title\"]\n\tif !ok && project == \"\" {\n\t\treturn fmt.Errorf(\"title key not found\")\n\t}\n\n\tprojectAndVersion := strings.SplitN(project, \":\", 2)\n\tif len(projectAndVersion) != 2 {\n\t\treturn fmt.Errorf(\"title key has wrong format\")\n\t}\n\n\tbom, err := json.Marshal(json.RawMessage(content[\"description\"]))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"description key has wrong format: %w\", err)\n\t}\n\n\tclient, err := dtrack.NewClient(dta.Url, dtrack.WithAPIKey(dta.APIKey))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create dependency track client: %w\", err)\n\t}\n\n\tctx := context.Background()\n\n\t_, err = client.BOM.Upload(ctx, dtrack.BOMUploadRequest{\n\t\tProjectName:    projectAndVersion[0],\n\t\tProjectVersion: projectAndVersion[1],\n\t\tAutoCreate:     true,\n\t\tBOM:            base64.StdEncoding.EncodeToString(bom),\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload BOM: %w\", err)\n\t}\n\n\tlog.Printf(\"successfully sent: %q to Dependency Track\", dta.Name)\n\n\treturn nil\n}\n\nfunc (dta *DependencyTrackAction) Terminate() error {\n\tlog.Printf(\"Dependency Track action %s terminated.\", dta.Name)\n\treturn nil\n}\n\nfunc (dta *DependencyTrackAction) GetLayoutProvider() layout.LayoutProvider {\n\treturn new(formatting.HtmlProvider)\n}\n"
  },
  {
    "path": "actions/dependencytrack_test.go",
    "content": "package actions\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDependencyTrackAction_Send(t *testing.T) {\n\tbomJSON := `{\n\t\t\"$schema\": \"http://cyclonedx.org/schema/bom-1.4.schema.json\",\n\t\t\"bomFormat\": \"CycloneDX\",\n\t\t\"specVersion\": \"1.4\",\n\t\t\"serialNumber\": \"urn:uuid:78f7eeb2-25fd-45ce-9ece-63cf0ca9b1af\",\n\t\t\"version\": 1,\n\t\t\"metadata\": {\n\t\t  \"timestamp\": \"2023-07-26T07:42:41+00:00\",\n\t\t  \"tools\": [\n\t\t\t{\n\t\t\t  \"vendor\": \"aquasecurity\",\n\t\t\t  \"name\": \"trivy\",\n\t\t\t  \"version\": \"0.43.1\"\n\t\t\t}\n\t\t  ],\n\t\t  \"component\": {\n\t\t\t\"bom-ref\": \"pkg:oci/busybox@sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a?repository_url=index.docker.io%2Flibrary%2Fbusybox\\u0026arch=arm64\",\n\t\t\t\"type\": \"container\",\n\t\t\t\"name\": \"busybox:latest\",\n\t\t\t\"purl\": \"pkg:oci/busybox@sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a?repository_url=index.docker.io%2Flibrary%2Fbusybox\\u0026arch=arm64\",\n\t\t\t\"properties\": [\n\t\t\t  {\n\t\t\t\t\"name\": \"aquasecurity:trivy:DiffID\",\n\t\t\t\t\"value\": \"sha256:57d0c5e3b21e4fdac106cfee383d702b92cd433e6e45588153228670b616bc59\"\n\t\t\t  },\n\t\t\t  {\n\t\t\t\t\"name\": \"aquasecurity:trivy:ImageID\",\n\t\t\t\t\"value\": \"sha256:d38589532d9756ff743d2149a143bfad79833261ff18c24b22088183a651ff65\"\n\t\t\t  },\n\t\t\t  {\n\t\t\t\t\"name\": \"aquasecurity:trivy:RepoDigest\",\n\t\t\t\t\"value\": \"busybox@sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a\"\n\t\t\t  },\n\t\t\t  {\n\t\t\t\t\"name\": \"aquasecurity:trivy:RepoTag\",\n\t\t\t\t\"value\": \"busybox:latest\"\n\t\t\t  },\n\t\t\t  {\n\t\t\t\t\"name\": \"aquasecurity:trivy:SchemaVersion\",\n\t\t\t\t\"value\": \"2\"\n\t\t\t  }\n\t\t\t]\n\t\t  }\n\t\t},\n\t\t\"components\": [],\n\t\t\"dependencies\": [\n\t\t  {\n\t\t\t\"ref\": \"pkg:oci/busybox@sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a?repository_url=index.docker.io%2Flibrary%2Fbusybox\\u0026arch=arm64\",\n\t\t\t\"dependsOn\": []\n\t\t  }\n\t\t],\n\t\t\"vulnerabilities\": []\n\t  }`\n\ttype fields struct {\n\t\tName   string\n\t\tUrl    string\n\t\tAPIKey string\n\t}\n\ttype args struct {\n\t\tcontent map[string]string\n\t}\n\ttests := []struct {\n\t\tname        string\n\t\tfields      fields\n\t\targs        args\n\t\twantErr     string\n\t\terrMsg      string\n\t\thandlerFunc http.HandlerFunc\n\t}{\n\t\t{\n\t\t\tname: \"valid content JSON BOM\",\n\t\t\tfields: fields{\n\t\t\t\tName:   \"test\",\n\t\t\t\tAPIKey: \"key\",\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tcontent: map[string]string{\n\t\t\t\t\t\"title\":       \"test-project:test-version\",\n\t\t\t\t\t\"description\": bomJSON,\n\t\t\t\t},\n\t\t\t},\n\t\t\thandlerFunc: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t_, err := w.Write([]byte(`{\"token\":\"6026693d-b182-4569-8ba1-0b2c0cc509be\"}`))\n\t\t\t\tif err != nil {\n\t\t\t\t\tpanic(err)\n\t\t\t\t}\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"not found title\",\n\t\t\tfields: fields{\n\t\t\t\tName:   \"test\",\n\t\t\t\tAPIKey: \"key\",\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tcontent: map[string]string{\n\t\t\t\t\t\"description\": bomJSON,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"title key not found\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid title format\",\n\t\t\tfields: fields{\n\t\t\t\tName:   \"test\",\n\t\t\t\tAPIKey: \"key\",\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tcontent: map[string]string{\n\t\t\t\t\t\"title\":       \"invalid\",\n\t\t\t\t\t\"description\": bomJSON,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"title key has wrong format\",\n\t\t},\n\t\t{\n\t\t\tname: \"invalid description format\",\n\t\t\tfields: fields{\n\t\t\t\tName:   \"test\",\n\t\t\t\tAPIKey: \"key\",\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tcontent: map[string]string{\n\t\t\t\t\t\"title\":       \"test-project:test-version\",\n\t\t\t\t\t\"description\": \"invalid\",\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"description key has wrong format: json: error calling MarshalJSON for type json.RawMessage: invalid character 'i' looking for beginning of value\",\n\t\t\thandlerFunc: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t}),\n\t\t},\n\t\t{\n\t\t\tname: \"failed to upload BOM\",\n\t\t\tfields: fields{\n\t\t\t\tName:   \"test\",\n\t\t\t\tAPIKey: \"invalid\",\n\t\t\t},\n\t\t\targs: args{\n\t\t\t\tcontent: map[string]string{\n\t\t\t\t\t\"title\":       \"test-project:test-version\",\n\t\t\t\t\t\"description\": bomJSON,\n\t\t\t\t},\n\t\t\t},\n\t\t\twantErr: \"failed to upload BOM: api error (status: 401)\",\n\t\t\thandlerFunc: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t}),\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tts := httptest.NewServer(tt.handlerFunc)\n\t\t\tdefer ts.Close()\n\n\t\t\turl := tt.fields.Url\n\t\t\tif url == \"\" {\n\t\t\t\turl = ts.URL\n\t\t\t}\n\n\t\t\tdta := &DependencyTrackAction{\n\t\t\t\tName:   tt.fields.Name,\n\t\t\t\tUrl:    url,\n\t\t\t\tAPIKey: tt.fields.APIKey,\n\t\t\t}\n\n\t\t\terr := dta.Send(tt.args.content)\n\t\t\tif tt.wantErr != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), tt.wantErr)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, tt.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "actions/docker.go",
    "content": "package actions\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/tidwall/gjson\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\t\"github.com/docker/docker/api/types\"\n\t\"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/docker/api/types/mount\"\n\t\"github.com/docker/docker/client\"\n\t\"github.com/docker/docker/pkg/stdcopy\"\n\t\"github.com/google/uuid\"\n)\n\ntype DockerClient struct {\n\tclient  client.APIClient\n\tuuidNew func() uuid.UUID\n\n\tName      string\n\tImageName string\n\tCmd       []string\n\tVolumes   map[string]string\n\tNetwork   string\n\tEnv       []string\n}\n\nfunc (d DockerClient) GetName() string {\n\treturn d.Name\n}\n\nfunc (d *DockerClient) Init() error {\n\tvar err error\n\td.client, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize docker action: %w\", err)\n\t}\n\td.uuidNew = uuid.New\n\n\tlog.Println(\"docker action successfully initialized\")\n\treturn nil\n}\n\nfunc (d DockerClient) Send(m map[string]string) error {\n\tctx := context.Background()\n\tparsedCmd := d.parseCmd(m)\n\n\tr, err := d.client.ImagePull(ctx, d.ImageName, types.ImagePullOptions{})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"docker action failed to pull docker image: %w\", err)\n\t}\n\tdefer r.Close()\n\n\tvar hc container.HostConfig\n\tif len(d.Volumes) > 0 {\n\t\tfor src, dst := range d.Volumes {\n\t\t\thc.Mounts = append(hc.Mounts, mount.Mount{Type: mount.TypeBind, Source: src, Target: dst})\n\t\t}\n\t}\n\tif len(d.Network) > 0 {\n\t\thc.NetworkMode = container.NetworkMode(d.Network)\n\t}\n\n\tenv := append(d.Env, fmt.Sprintf(`POSTEE_EVENT=\"%s\"`, m[\"description\"]))\n\n\tctrName := fmt.Sprintf(\"postee-%s-%s\", d.GetName(), d.uuidNew())\n\t_, err = d.client.ContainerCreate(ctx, &container.Config{\n\t\tImage: d.ImageName,\n\t\tCmd:   parsedCmd,\n\t\tEnv:   env,\n\t}, &hc, nil, nil, ctrName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"docker action failed to create docker container: %w\", err)\n\t}\n\tdefer func() {\n\t\t_ = d.client.ContainerRemove(ctx, ctrName, types.ContainerRemoveOptions{Force: true})\n\t}()\n\tif err := d.client.ContainerStart(ctx, ctrName, types.ContainerStartOptions{}); err != nil {\n\t\treturn fmt.Errorf(\"docker action failed to start container: %w\", err)\n\t}\n\n\tstatusCh, errCh := d.client.ContainerWait(ctx, ctrName, container.WaitConditionNotRunning)\n\tselect {\n\tcase err := <-errCh:\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"docker action failed running container: %w\", err)\n\t\t}\n\tcase <-statusCh:\n\t}\n\n\tout, err := d.client.ContainerLogs(ctx, ctrName, types.ContainerLogsOptions{\n\t\tShowStdout: true})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"docker action unable to fetch container logs: %w\", err)\n\t}\n\n\tvar buf bytes.Buffer\n\t_, _ = stdcopy.StdCopy(&buf, &buf, out)\n\tlog.Println(\"docker action ran successfully, container logs: \", buf.String())\n\treturn nil\n}\n\nfunc (d DockerClient) Terminate() error {\n\tif err := d.client.Close(); err != nil {\n\t\treturn fmt.Errorf(\"docker action unable to terminate: %w\", err)\n\t}\n\tlog.Println(\"docker action terminated successfully\")\n\treturn nil\n}\n\nfunc (d DockerClient) GetLayoutProvider() layout.LayoutProvider {\n\treturn nil\n}\n\nfunc (d DockerClient) parseCmd(input map[string]string) (parsedCmds []string) {\n\tfor _, c := range d.Cmd {\n\t\tvar calcVal string\n\t\tif strings.HasPrefix(c, regoInputPrefix) {\n\t\t\tif ok := json.Valid([]byte(input[\"description\"])); ok { // input is json\n\t\t\t\tcalcVal = gjson.Get(input[\"description\"], strings.TrimPrefix(c, regoInputPrefix+\".\")).String()\n\t\t\t} else {\n\t\t\t\tcalcVal = input[\"description\"] // input is a string\n\t\t\t}\n\t\t} else {\n\t\t\tcalcVal = c // no rego to parse\n\t\t}\n\t\tparsedCmds = append(parsedCmds, calcVal)\n\t}\n\treturn\n}\n"
  },
  {
    "path": "actions/docker_test.go",
    "content": "package actions\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/docker/docker/api/types\"\n\tcontainertypes \"github.com/docker/docker/api/types/container\"\n\t\"github.com/docker/docker/api/types/mount\"\n\tnetworktypes \"github.com/docker/docker/api/types/network\"\n\t\"github.com/docker/docker/client\"\n\t\"github.com/google/uuid\"\n\tspecs \"github.com/opencontainers/image-spec/specs-go/v1\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype mockDockerClient struct {\n\tclient.APIClient\n\n\timagePull       func(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error)\n\tcontainerCreate func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error)\n\tcontainerStart  func(ctx context.Context, container string, options types.ContainerStartOptions) error\n\tcontainerWait   func(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.ContainerWaitOKBody, <-chan error)\n\tcontainerLogs   func(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error)\n\tcontainerRemove func(ctx context.Context, container string, options types.ContainerRemoveOptions) error\n}\n\nfunc (m mockDockerClient) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) {\n\tif m.imagePull != nil {\n\t\treturn m.imagePull(ctx, ref, options)\n\t}\n\n\treturn io.NopCloser(strings.NewReader(`pulling image foo bar`)), nil\n}\n\nfunc (m mockDockerClient) ContainerCreate(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) {\n\tif m.containerCreate != nil {\n\t\treturn m.containerCreate(ctx, config, hostConfig, networkingConfig, platform, containerName)\n\t}\n\n\treturn containertypes.ContainerCreateCreatedBody{\n\t\tID: \"foo-bar-123\",\n\t}, nil\n}\n\nfunc (m mockDockerClient) ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error {\n\tif m.containerStart != nil {\n\t\treturn m.containerStart(ctx, container, options)\n\t}\n\n\treturn nil\n}\n\nfunc (m mockDockerClient) ContainerWait(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.ContainerWaitOKBody, <-chan error) {\n\tif m.containerWait != nil {\n\t\treturn m.containerWait(ctx, container, condition)\n\t}\n\n\tresultC := make(chan containertypes.ContainerWaitOKBody)\n\terrC := make(chan error)\n\n\tgo func() {\n\t\tresultC <- containertypes.ContainerWaitOKBody{\n\t\t\tError:      nil,\n\t\t\tStatusCode: http.StatusOK,\n\t\t}\n\t\terrC <- nil\n\t}()\n\n\treturn resultC, errC\n}\n\nfunc (m mockDockerClient) ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {\n\tif m.containerLogs != nil {\n\t\treturn m.containerLogs(ctx, container, options)\n\t}\n\n\treturn io.NopCloser(strings.NewReader(\"the logs of joy\")), nil\n}\n\nfunc (m mockDockerClient) ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error {\n\tif m.containerRemove != nil {\n\t\treturn m.containerRemove(ctx, container, options)\n\t}\n\n\treturn nil\n}\n\ntype mockUUID struct {\n}\n\nfunc (mockUUID) New() uuid.UUID {\n\treturn uuid.MustParse(\"1471d64a-6c64-4527-bbd8-7bc772678db8\")\n}\n\nfunc TestDocketClient_Send(t *testing.T) {\n\ttestCases := []struct {\n\t\tname           string\n\t\tinputEvent     string\n\t\tinputDockerCmd []string\n\n\t\timagePullFunc       func(context.Context, string, types.ImagePullOptions) (io.ReadCloser, error)\n\t\tcontainerCreateFunc func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error)\n\t\tcontainerRemoveFunc func(ctx context.Context, container string, options types.ContainerRemoveOptions) error\n\t\tcontainerWaitFunc   func(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.ContainerWaitOKBody, <-chan error)\n\t\tcontainerStartFunc  func(ctx context.Context, container string, options types.ContainerStartOptions) error\n\t\tcontainerLogsFunc   func(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error)\n\n\t\texpectedError string\n\t\texpectedLogs  string\n\t}{\n\t\t{\n\t\t\tname:         \"happy path, string input event\",\n\t\t\tinputEvent:   `foo bar baz`,\n\t\t\texpectedLogs: \"the logs of joy\",\n\t\t\tcontainerCreateFunc: func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) {\n\n\t\t\t\tassert.Equal(t, containertypes.Config{\n\t\t\t\t\tImage: \"docker.io/library/alpine\",\n\t\t\t\t\tCmd:   []string{\"echo\", \"hello world\"},\n\t\t\t\t\tEnv:   []string{\"FOO=bar\", `POSTEE_EVENT=\"foo bar baz\"`},\n\t\t\t\t}, *config)\n\n\t\t\t\tassert.Equal(t, containertypes.HostConfig{\n\t\t\t\t\tMounts: []mount.Mount{{Type: mount.TypeBind, Source: \"foo-src\", Target: \"bar-dst\"}}, NetworkMode: \"host\",\n\t\t\t\t}, *hostConfig)\n\n\t\t\t\tassert.Contains(t, containerName, \"postee-my-docker-action\")\n\n\t\t\t\treturn containertypes.ContainerCreateCreatedBody{\n\t\t\t\t\tID: \"foo-bar-123\",\n\t\t\t\t}, nil\n\t\t\t},\n\t\t\tcontainerRemoveFunc: func(ctx context.Context, container string, options types.ContainerRemoveOptions) error {\n\n\t\t\t\tassert.Equal(t, \"postee-my-docker-action-1471d64a-6c64-4527-bbd8-7bc772678db8\", container)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:           \"happy path, relative json input event\",\n\t\t\tinputEvent:     `{\"hostname\":\"foo.host\"}`,\n\t\t\tinputDockerCmd: []string{\"kubectl\", \"delete\", \"pod\", \"event.input.hostname\"},\n\t\t\texpectedLogs:   \"the logs of joy\",\n\t\t\tcontainerCreateFunc: func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) {\n\n\t\t\t\tassert.Equal(t, containertypes.Config{\n\t\t\t\t\tImage: \"docker.io/library/alpine\",\n\t\t\t\t\tCmd:   []string{\"kubectl\", \"delete\", \"pod\", \"foo.host\"},\n\t\t\t\t\tEnv:   []string{\"FOO=bar\", `POSTEE_EVENT=\"{\"hostname\":\"foo.host\"}\"`},\n\t\t\t\t}, *config)\n\n\t\t\t\tassert.Equal(t, containertypes.HostConfig{\n\t\t\t\t\tMounts: []mount.Mount{{Type: mount.TypeBind, Source: \"foo-src\", Target: \"bar-dst\"}}, NetworkMode: \"host\",\n\t\t\t\t}, *hostConfig)\n\n\t\t\t\tassert.Contains(t, containerName, \"postee-my-docker-action\")\n\n\t\t\t\treturn containertypes.ContainerCreateCreatedBody{\n\t\t\t\t\tID: \"foo-bar-123\",\n\t\t\t\t}, nil\n\t\t\t},\n\t\t\tcontainerRemoveFunc: func(ctx context.Context, container string, options types.ContainerRemoveOptions) error {\n\n\t\t\t\tassert.Equal(t, \"postee-my-docker-action-1471d64a-6c64-4527-bbd8-7bc772678db8\", container)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"sad path, ImagePull returns an error\",\n\t\t\timagePullFunc: func(ctx context.Context, s string, options types.ImagePullOptions) (io.ReadCloser, error) {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to pull image\")\n\t\t\t},\n\t\t\texpectedError: \"docker action failed to pull docker image: failed to pull image\",\n\t\t},\n\t\t{\n\t\t\tname: \"sad path, ContainerCreate returns an error\",\n\t\t\tcontainerCreateFunc: func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) {\n\t\t\t\treturn containertypes.ContainerCreateCreatedBody{}, fmt.Errorf(\"container creation failed\")\n\t\t\t},\n\t\t\texpectedError: \"docker action failed to create docker container: container creation failed\",\n\t\t},\n\t\t{\n\t\t\tname: \"sad path, ContainerStart returns an error\",\n\t\t\tcontainerStartFunc: func(ctx context.Context, container string, options types.ContainerStartOptions) error {\n\t\t\t\treturn fmt.Errorf(\"failed to start\")\n\t\t\t},\n\t\t\texpectedError: \"docker action failed to start container: failed to start\",\n\t\t},\n\t\t{\n\t\t\tname: \"sad path, ContainerWait returns an error\",\n\t\t\tcontainerWaitFunc: func(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.ContainerWaitOKBody, <-chan error) {\n\n\t\t\t\terrC := make(chan error)\n\t\t\t\tgo func() {\n\t\t\t\t\terrC <- fmt.Errorf(\"failed to wait\")\n\t\t\t\t}()\n\t\t\t\treturn nil, errC\n\n\t\t\t},\n\t\t\texpectedError: \"docker action failed running container: failed to wait\",\n\t\t},\n\t\t{\n\t\t\tname: \"sad path, ContainerLogs returns an error\",\n\t\t\tcontainerLogsFunc: func(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to get logs\")\n\t\t\t},\n\t\t\texpectedError: \"docker action unable to fetch container logs: failed to get logs\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tdc := DockerClient{\n\t\t\t\tName:      \"my-docker-action\",\n\t\t\t\tImageName: \"docker.io/library/alpine\",\n\t\t\t\tEnv:       []string{\"FOO=bar\"},\n\t\t\t\tNetwork:   \"host\",\n\t\t\t\tVolumes: map[string]string{\n\t\t\t\t\t\"foo-src\": \"bar-dst\",\n\t\t\t\t},\n\t\t\t\tclient: &mockDockerClient{\n\t\t\t\t\timagePull:       tc.imagePullFunc,\n\t\t\t\t\tcontainerCreate: tc.containerCreateFunc,\n\t\t\t\t\tcontainerRemove: tc.containerRemoveFunc,\n\t\t\t\t\tcontainerWait:   tc.containerWaitFunc,\n\t\t\t\t\tcontainerStart:  tc.containerStartFunc,\n\t\t\t\t\tcontainerLogs:   tc.containerLogsFunc,\n\t\t\t\t},\n\t\t\t\tuuidNew: mockUUID{}.New,\n\t\t\t}\n\n\t\t\tswitch {\n\t\t\tcase tc.inputDockerCmd != nil:\n\t\t\t\tdc.Cmd = tc.inputDockerCmd\n\t\t\tdefault:\n\t\t\t\tdc.Cmd = []string{\"echo\", \"hello world\"}\n\t\t\t}\n\n\t\t\terr := dc.Send(map[string]string{\"description\": tc.inputEvent})\n\t\t\tif tc.expectedError != \"\" {\n\t\t\t\tassert.Equal(t, tc.expectedError, err.Error(), tc.name)\n\t\t\t} else {\n\t\t\t\tassert.NoError(t, err, tc.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "actions/email.go",
    "content": "package actions\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/smtp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nvar (\n\terrThereIsNoRecipient = errors.New(\"there is no recipient\")\n\tlookupMXFunc          = net.LookupMX\n)\n\ntype EmailAction struct {\n\tName           string\n\tUser           string\n\tPassword       string\n\tHost           string\n\tPort           int\n\tSender         string\n\tRecipients     []string\n\tClientHostName string\n\tUseMX          bool\n\tsendFunc       func(addr string, a smtp.Auth, from string, to []string, msg []byte) error\n}\n\nfunc (email *EmailAction) GetName() string {\n\treturn email.Name\n}\n\nfunc (email *EmailAction) Init() error {\n\tlog.Printf(\"Starting Email action %q...\", email.Name)\n\tif email.Sender == \"\" {\n\t\temail.Sender = email.User\n\t}\n\tif email.ClientHostName != \"\" {\n\t\tlog.Printf(\"Action %q uses a custom client name %q instead of `localhost`\", email.Name, email.ClientHostName)\n\t\temail.sendFunc = email.sendEmailWithCustomClient\n\t} else {\n\t\temail.sendFunc = smtp.SendMail\n\t}\n\n\treturn nil\n}\n\nfunc (email *EmailAction) Terminate() error {\n\tlog.Printf(\"Email action terminated\\n\")\n\treturn nil\n}\n\nfunc (email *EmailAction) GetLayoutProvider() layout.LayoutProvider {\n\treturn new(formatting.HtmlProvider)\n}\n\nfunc (email *EmailAction) Send(content map[string]string) error {\n\tsubject := content[\"title\"]\n\tbody := content[\"description\"]\n\tport := strconv.Itoa(email.Port)\n\trecipients := getHandledRecipients(email.Recipients, &content, email.Name)\n\tif len(recipients) == 0 {\n\t\treturn errThereIsNoRecipient\n\t}\n\n\tmsg := fmt.Sprintf(\n\t\t\"To: %s\\r\\n\"+\n\t\t\t\"From: %s\\r\\n\"+\n\t\t\t\"Subject: %s\\r\\n\"+\n\t\t\t\"Content-Type: text/html; charset=UTF-8\\r\\n\\r\\n%s\\r\\n\",\n\t\tstrings.Join(recipients, \",\"), email.Sender, subject, body)\n\n\tif email.UseMX {\n\t\temail.sendViaMxServers(port, msg, recipients)\n\t\treturn nil\n\t}\n\n\tvar auth smtp.Auth\n\tif len(email.Password) > 0 && len(email.User) > 0 {\n\t\tauth = smtp.PlainAuth(\"\", email.User, email.Password, email.Host)\n\t}\n\n\terr := email.sendFunc(email.Host+\":\"+port, auth, email.Sender, recipients, []byte(msg))\n\tif err != nil {\n\t\tlog.Println(\"SendMail Error:\", err)\n\t\tlog.Printf(\"From: %q, to %v via %q\", email.Sender, email.Recipients, email.Host)\n\t\treturn err\n\t}\n\tlog.Println(\"Email was sent successfully!\")\n\treturn nil\n}\n\n// sendEmailWithCustomClient replaces smtp.SendMail() in cases\n// where it is necessary to establish a custom client host name instead of \"localhost\",\n// while keeping the remaining behavior unchanged.\nfunc (email EmailAction) sendEmailWithCustomClient(addr string, a smtp.Auth, from string, to []string, msg []byte) error {\n\tlog.Printf(\"Sending an email via Custom client for action %q\", email.Name)\n\n\tc, err := smtp.Dial(addr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer c.Close()\n\n\tif err := c.Hello(email.ClientHostName); err != nil {\n\t\treturn err\n\t}\n\n\tif ok, _ := c.Extension(\"STARTTLS\"); ok {\n\t\tconfig := &tls.Config{ServerName: email.Host}\n\t\tif err = c.StartTLS(config); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif a != nil {\n\t\tif err = c.Auth(a); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err = c.Mail(from); err != nil {\n\t\treturn err\n\t}\n\tfor _, addr := range to {\n\t\tif err = c.Rcpt(addr); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tw, err := c.Data()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = w.Write(msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\terr = w.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn c.Quit()\n}\n\nfunc (email EmailAction) sendViaMxServers(port string, msg string, recipients []string) {\n\tfor _, rcpt := range recipients {\n\t\tat := strings.LastIndex(rcpt, \"@\")\n\t\tif at < 0 {\n\t\t\tlog.Printf(\"%q isn't email\", rcpt)\n\t\t\tcontinue\n\t\t}\n\n\t\thost := rcpt[at+1:]\n\t\tmxs, err := lookupMXFunc(host)\n\t\tif err != nil {\n\t\t\tlog.Println(\"error looking up mx host: \", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, mx := range mxs {\n\t\t\tif err := email.sendFunc(mx.Host+\":\"+port, nil, email.Sender, recipients, []byte(msg)); err != nil {\n\t\t\t\tlog.Printf(\"SendMail error to %q via %q\", rcpt, mx.Host)\n\t\t\t\tlog.Println(\"error: \", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Printf(\"The message to %q was sent successful via %q!\", rcpt, mx.Host)\n\t\t\tbreak\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "actions/email_test.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/smtp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc mockSend(errToReturn error, emailSent *int) (func(string, smtp.Auth, string, []string, []byte) error, *emailRecorder) {\n\tr := new(emailRecorder)\n\treturn func(addr string, a smtp.Auth, from string, to []string, msg []byte) error {\n\t\t*r = emailRecorder{addr, a, from, to, msg}\n\t\tif errToReturn == nil {\n\t\t\t*emailSent++\n\t\t}\n\t\treturn errToReturn\n\t}, r\n}\n\ntype emailRecorder struct {\n\taddr string\n\tauth smtp.Auth\n\tfrom string\n\tto   []string\n\tmsg  []byte\n}\n\nfunc TestEmailAction_Send(t *testing.T) {\n\ttestCases := []struct {\n\t\tname               string\n\t\tlookupMXFunc       func(name string) ([]*net.MX, error)\n\t\temailAction        *EmailAction\n\t\texpectedMessage    string\n\t\tsendError          error\n\t\texpectedError      error\n\t\texpectedSentEmails int\n\t}{\n\t\t{\n\t\t\tname: \"happy path, with auth, server supports auth\",\n\t\t\texpectedMessage: fmt.Sprintf(\"To: anything@fubar.com\\r\\n\" +\n\t\t\t\t\"From: sender@mailer.com\\r\\n\" +\n\t\t\t\t\"Subject: email subject\\r\\n\" +\n\t\t\t\t\"Content-Type: text/html; charset=UTF-8\\r\\n\" +\n\t\t\t\t\"\\r\\n\" +\n\t\t\t\t\"foo bar baz body\\r\\n\"),\n\t\t\texpectedSentEmails: 1,\n\t\t},\n\t\t{\n\t\t\tname: \"happy path, use multiple mx servers, no auth\",\n\t\t\tlookupMXFunc: func(name string) ([]*net.MX, error) {\n\t\t\t\treturn []*net.MX{\n\t\t\t\t\t{\n\t\t\t\t\t\tHost: \"127.0.0.1\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tHost: \"128.0.0.1\",\n\t\t\t\t\t},\n\t\t\t\t}, nil\n\t\t\t},\n\t\t\texpectedMessage: fmt.Sprintf(\"To: anything@fubar.com\\r\\n\" +\n\t\t\t\t\"From: sender@mailer.com\\r\\n\" +\n\t\t\t\t\"Subject: email subject\\r\\n\" +\n\t\t\t\t\"Content-Type: text/html; charset=UTF-8\\r\\n\" +\n\t\t\t\t\"\\r\\n\" +\n\t\t\t\t\"foo bar baz body\\r\\n\"),\n\t\t\texpectedSentEmails: 1,\n\t\t},\n\t\t{\n\t\t\tname:          \"sad path, no recipients\",\n\t\t\temailAction:   &EmailAction{Recipients: []string{}},\n\t\t\texpectedError: errThereIsNoRecipient,\n\t\t},\n\t\t{\n\t\t\tname:               \"sad path, client uses AUTH, smtp server does not support AUTH\",\n\t\t\tsendError:          fmt.Errorf(\"smtp: server doesn't support AUTH\"),\n\t\t\texpectedError:      fmt.Errorf(\"smtp: server doesn't support AUTH\"),\n\t\t\texpectedMessage:    \"\",\n\t\t\texpectedSentEmails: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"sad path, use mx server, invalid recipient,\",\n\t\t\temailAction: &EmailAction{\n\t\t\t\tName:       \"my-email\",\n\t\t\t\tUser:       \"user\",\n\t\t\t\tPassword:   \"pass\",\n\t\t\t\tHost:       \"127.0.0.1\",\n\t\t\t\tPort:       587,\n\t\t\t\tSender:     \"sender@mailer.com\",\n\t\t\t\tRecipients: []string{\"invalid recipient\"},\n\t\t\t\tUseMX:      true,\n\t\t\t},\n\t\t\texpectedSentEmails: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"sad path, no mx server available\",\n\t\t\tlookupMXFunc: func(name string) ([]*net.MX, error) {\n\t\t\t\treturn []*net.MX{}, fmt.Errorf(\"no such host\")\n\t\t\t},\n\t\t\texpectedSentEmails: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"sad path, use mx servers, error sending email\",\n\t\t\tlookupMXFunc: func(name string) ([]*net.MX, error) {\n\t\t\t\treturn []*net.MX{\n\t\t\t\t\t{\n\t\t\t\t\t\tHost: \"127.0.0.1\",\n\t\t\t\t\t},\n\t\t\t\t}, nil\n\t\t\t},\n\t\t\texpectedMessage: fmt.Sprintf(\"To: anything@fubar.com\\r\\n\" +\n\t\t\t\t\"From: sender@mailer.com\\r\\n\" +\n\t\t\t\t\"Subject: email subject\\r\\n\" +\n\t\t\t\t\"Content-Type: text/html; charset=UTF-8\\r\\n\" +\n\t\t\t\t\"\\r\\n\" +\n\t\t\t\t\"foo bar baz body\\r\\n\"),\n\t\t\tsendError:          fmt.Errorf(\"internal server error\"),\n\t\t\texpectedSentEmails: 0,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar eo EmailAction\n\t\t\tif tc.emailAction != nil {\n\t\t\t\teo = *tc.emailAction\n\t\t\t} else {\n\t\t\t\teo = EmailAction{\n\t\t\t\t\tName:       \"my-email\",\n\t\t\t\t\tUser:       \"user\",\n\t\t\t\t\tPassword:   \"pass\",\n\t\t\t\t\tHost:       \"127.0.0.1\",\n\t\t\t\t\tPort:       587,\n\t\t\t\t\tSender:     \"sender@mailer.com\",\n\t\t\t\t\tRecipients: []string{\"anything@fubar.com\"},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tvar emailsSent int\n\t\t\tf, r := mockSend(tc.sendError, &emailsSent)\n\t\t\teo.sendFunc = f\n\n\t\t\tif tc.lookupMXFunc != nil {\n\t\t\t\toldLookupMXFunc := lookupMXFunc\n\t\t\t\tlookupMXFunc = tc.lookupMXFunc\n\t\t\t\tdefer func() {\n\t\t\t\t\tlookupMXFunc = oldLookupMXFunc\n\t\t\t\t}()\n\t\t\t\teo.UseMX = true\n\t\t\t}\n\n\t\t\terr := eo.Send(map[string]string{\"description\": \"foo bar baz body\", \"title\": \"email subject\"})\n\t\t\tswitch {\n\t\t\tcase tc.expectedError != nil:\n\t\t\t\tassert.Equal(t, tc.expectedError, err, tc.name)\n\t\t\t\tassert.Equal(t, tc.expectedSentEmails, emailsSent, tc.name)\n\t\t\tdefault:\n\t\t\t\tassert.NoError(t, err, tc.name)\n\t\t\t\tassert.Equal(t, tc.expectedSentEmails, emailsSent, tc.name)\n\t\t\t\tassert.Equal(t, tc.expectedMessage, string(r.msg), tc.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "actions/example/exec/defectdojo-curl-upload-scan.sh",
    "content": "#!/usr/bin/env sh\n\n# this shell script is meant to be executed by a Aquasec/Postee \"exec\"\n# action, the event data is passed in through environment variable\n# POSTEE_EVENT\n#\n# Requirements on JSON format\n# ---------------------------\n# - JSON dictionary with \"defectdojo\" as top-level key\n# - \"defectdojo\" dictionary holds at least 2 keys\n#   - \"scan\", containing the report\n#   - \"metadata\", containing key/value pairs\n#\n# Required parameter\n# ------------------\n# - DEFECTDOJO_URL - Defectdojo URL, base URL, script appends path for v2\n# - DEFECTDOJO_API_TOKEN\n# - POSTEE_EVENT - variable containing the JSON content from template stage\n\n\nTEMP_PREFIX=\"/tmp/dd-scan-\"\n\nif [ -z \"$DEFECTDOJO_API_TOKEN\" ]; then\n  echo \"ERROR: could not find environment variable DEFECTDOJO_API_TOKEN\"\n  exit 1\nfi\n\nif [ -z \"$DEFECTDOJO_URL\" ]; then\n  echo \"could not find environment variable DEFECTDOJO_URL\" \n  exit 1\nfi\n\nif [ -z \"$POSTEE_EVENT\" ]; then\n  echo \"could not read any input data from POSTEE_EVENT\"\n  exit 1\nfi\n\n# shellcheck disable=SC2317 # used in signal trap for EXIT\n_cleanup() {\n  rm -f \"${TEMP_PREFIX}*\"\n}\n\ntrap _cleanup EXIT\n\n# write a temporary file with content received from POSTEE_EVENT\nTMP_FILE=\"$(mktemp ${TEMP_PREFIX}XXXXXX)\"\n\n_validate_json()\n{\n  if echo \"$POSTEE_EVENT\" | jq '.defectdojo.scan' | grep 'null' 1>/dev/null; then\n    echo \"ERROR => JSON, unexpected structure \\\"defectdojo\\\"\"\n    return 1\n  fi\n}\nif ! _validate_json; then\n  exit 1\nfi\n\necho \"$POSTEE_EVENT\" | jq '.defectdojo.scan' | tee \"$TMP_FILE\"\n\n# Initialize the command string\nCOMMAND=\"curl -X POST -H \\\"Authorization: Token $DEFECTDOJO_API_TOKEN\\\"\"\n\n# extract all key/value pairs from metadata key\n# convert the resulting dictionary into multiline\n# string => $key=$value, can further be consumed\n# in a FOR loop generating a FORM entry per row\nFORM_ENTRIES=$(echo \"$POSTEE_EVENT\" | jq '.defectdojo.metadata | keys_unsorted[] as $k | \"\\($k)=\\( .[$k])\"')\n\n# to be able to ignore whitespaces in values,\n# separator for FOR loops is configured to\n# a newline character, remove unset IFS\nOLD_IFS=\"$IFS\"\n# shellcheck disable=SC3003\nIFS=$'\\n'\nfor entry in $FORM_ENTRIES; do\n  COMMAND=\"$COMMAND -F $entry\"\ndone\nIFS=\"$OLD_IFS\"\n\nDD_IMPORT_URL=\"${DEFECTDOJO_URL}/api/v2/import-scan/\"\n\n# add URL and final JSON payload (trivy report)\nCOMMAND=\"$COMMAND -F \\\"file=@${TMP_FILE}\\\" ${DD_IMPORT_URL}\"\n\nif ! eval \"$COMMAND\"; then\n  echo \"ERROR: failed to send scan-report to ${DD_IMPORT_URL}\"\n  exit 1\nfi\n\necho \"SUCCESS: send scan-report to ${DD_IMPORT_URL}\"\nexit 0\n"
  },
  {
    "path": "actions/exec.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\ntype execCmd = func(string, ...string) *exec.Cmd\n\ntype ExecClient struct {\n\tExecCmd    execCmd\n\tName       string\n\tEnv        []string\n\tInputFile  string\n\tExecScript string\n\tAction     []byte\n}\n\nfunc (e *ExecClient) GetName() string {\n\treturn e.Name\n}\n\nfunc (e *ExecClient) Init() error {\n\te.ExecCmd = exec.Command\n\treturn nil\n}\n\nfunc (e *ExecClient) Send(m map[string]string) error {\n\tenvVars := os.Environ()\n\tenvVars = append(envVars, e.Env...)\n\tenvVars = append(envVars, fmt.Sprintf(\"POSTEE_EVENT=%s\", m[\"description\"]))\n\n\tvar cmd *exec.Cmd\n\tif len(e.InputFile) > 0 {\n\t\tcmd = e.ExecCmd(\"/bin/sh\", e.InputFile)\n\t\tcmd.Env = append(cmd.Env, envVars...)\n\t}\n\tif len(e.ExecScript) > 0 {\n\t\tcmd = e.ExecCmd(\"/bin/sh\")\n\t\tcmd.Env = append(cmd.Env, envVars...)\n\t\tcmd.Stdin = strings.NewReader(e.ExecScript)\n\t}\n\n\tvar err error\n\tif e.Action, err = cmd.CombinedOutput(); err != nil {\n\t\treturn fmt.Errorf(\"error while executing script: %w, output: %s\", err, string(e.Action))\n\t}\n\tlog.Println(\"execution output: \", \"len: \", len(e.Action), \"out: \", string(e.Action))\n\treturn nil\n}\n\nfunc (e *ExecClient) Terminate() error {\n\tlog.Printf(\"Exec action %s terminated\\n\", e.GetName())\n\treturn nil\n}\n\nfunc (e *ExecClient) GetLayoutProvider() layout.LayoutProvider {\n\t// Todo: This is MOCK. Because Formatting isn't need for Webhook\n\t// todo: The App should work with `return nil`\n\treturn new(formatting.HtmlProvider)\n}\n"
  },
  {
    "path": "actions/exec_test.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc fakeExecCmdFailure(command string, args ...string) *exec.Cmd {\n\tcs := []string{\"-test.run=TestShellProcessFail\", \"--\", command}\n\tcs = append(cs, args...)\n\tcmd := exec.Command(os.Args[0], cs...)\n\tcmd.Env = []string{\"GO_TEST_PROCESS=1\"}\n\treturn cmd\n}\n\nfunc TestShellProcessFail(t *testing.T) {\n\tif os.Getenv(\"GO_TEST_PROCESS\") != \"1\" {\n\t\treturn\n\t}\n\tfmt.Fprint(os.Stderr, \"failure\")\n\tos.Exit(1)\n}\n\nfunc TestExecClient_Init(t *testing.T) {\n\tec := ExecClient{}\n\trequire.NoError(t, ec.Init())\n}\n\nfunc TestExecClient_GetName(t *testing.T) {\n\tec := ExecClient{Name: \"my-exec-action\"}\n\trequire.NoError(t, ec.Init())\n\trequire.Equal(t, \"my-exec-action\", ec.GetName())\n}\n\nfunc TestExecClient_Send(t *testing.T) {\n\tt.Run(\"happy path\", func(t *testing.T) {\n\t\tf, err := ioutil.TempFile(\"\", \"TestExecClient_Send-*\")\n\t\trequire.NoError(t, err)\n\t\tdefer func() { os.RemoveAll(f.Name()) }()\n\t\t_, _ = f.WriteString(`#!/bin/sh\necho \"foo\"\necho $POSTEE_EVENT\necho $INPUT_ENV`)\n\n\t\tec := ExecClient{\n\t\t\tExecCmd:   exec.Command,\n\t\t\tInputFile: f.Name(),\n\t\t\tEnv:       []string{\"INPUT_ENV=input foo env var\"},\n\t\t}\n\t\trequire.NoError(t, ec.Send(map[string]string{\n\t\t\t\"description\": \"foo bar baz env variable\",\n\t\t}))\n\n\t\tassert.Equal(t, `foo\nfoo bar baz env variable\ninput foo env var\n`, string(ec.Action))\n\t\tassert.Equal(t, ec.Env, []string{\"INPUT_ENV=input foo env var\"})\n\t})\n\n\tt.Run(\"sad path - exec fails\", func(t *testing.T) {\n\t\tec := ExecClient{\n\t\t\tExecScript: `#!/bin/sh\necho \"foo bar baz\"`,\n\t\t\tExecCmd: fakeExecCmdFailure,\n\t\t}\n\t\trequire.EqualError(t, ec.Send(map[string]string{\n\t\t\t\"description\": \"foo bar baz\",\n\t\t}), \"error while executing script: exit status 1, output: failure\")\n\t})\n\n}\n"
  },
  {
    "path": "actions/goldens/validbody.txt",
    "content": "foo bar baz body"
  },
  {
    "path": "actions/http.go",
    "content": "package actions\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\t\"github.com/tidwall/gjson\"\n)\n\nvar (\n\tregoInputRegex = fmt.Sprintf(`(%s).*(.*)`, regoInputPrefix)\n)\n\ntype HTTPClient struct {\n\tName        string\n\tClient      http.Client\n\tURL         *url.URL\n\tMethod      string\n\tBodyFile    string\n\tBodyContent string\n\tHeaders     map[string][]string\n}\n\nfunc (hc *HTTPClient) GetName() string {\n\treturn hc.Name\n}\n\nfunc (hc *HTTPClient) Init() error {\n\treturn nil\n}\n\nfunc (hc HTTPClient) Send(m map[string]string) error {\n\t// encode headers as base64 to conform HTTP spec\n\t// https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2\n\tpe := base64.StdEncoding.EncodeToString([]byte(m[\"description\"]))\n\n\treq, err := http.NewRequest(hc.Method, hc.URL.String(), nil)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to initialize http request err: %w\", err)\n\t}\n\n\treq.Header.Add(\"Postee-Event\", pe) // preserve and transmit postee header\n\tfor k, vals := range hc.Headers {\n\t\tfor _, val := range vals {\n\t\t\treq.Header.Add(k, val)\n\t\t}\n\t}\n\n\tif len(hc.BodyFile) > 0 {\n\t\tbf, err := os.Open(hc.BodyFile)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to read body file: %s, err: %w\", hc.BodyFile, err)\n\t\t}\n\t\treq.Body = bf\n\t}\n\n\tif len(hc.BodyContent) > 0 {\n\t\treq.Body = io.NopCloser(strings.NewReader(parseBody(m, hc.BodyContent)))\n\t}\n\n\tresp, err := hc.Client.Do(req)\n\tif err != nil {\n\t\tlog.Println(\"error during HTTP Client execution: \", err.Error())\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to read HTTP response: %w\", err)\n\t}\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\treturn fmt.Errorf(\"http status NOT OK: HTTP %d %s, response: %s\", resp.StatusCode, http.StatusText(resp.StatusCode), string(b))\n\t}\n\n\tlog.Printf(\"http %s execution to url %s successful\", hc.Method, hc.URL)\n\treturn nil\n}\n\nfunc parseBody(inputEvent map[string]string, bodyContent string) string {\n\tre := regexp.MustCompile(regoInputRegex)\n\tsubs := re.FindAllString(bodyContent, -1)\n\tif subs == nil {\n\t\treturn bodyContent\n\t}\n\n\tfor _, sub := range subs {\n\t\tif ok := json.Valid([]byte(inputEvent[\"description\"])); ok {\n\t\t\tbodyContent = strings.Replace(bodyContent, sub, gjson.Get(inputEvent[\"description\"], strings.TrimPrefix(sub, \"event.input.\")).String(), 1)\n\t\t} else {\n\t\t\tbodyContent = strings.Replace(bodyContent, \"event.input\", inputEvent[\"description\"], 1)\n\t\t}\n\t}\n\n\treturn bodyContent\n}\n\nfunc (hc HTTPClient) Terminate() error {\n\tlog.Printf(\"HTTP action terminated\\n\")\n\treturn nil\n}\n\nfunc (hc HTTPClient) GetLayoutProvider() layout.LayoutProvider {\n\treturn nil\n}\n"
  },
  {
    "path": "actions/http_test.go",
    "content": "package actions\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHTTPClient_Init(t *testing.T) {\n\tec := HTTPClient{}\n\trequire.NoError(t, ec.Init())\n}\n\nfunc TestHTTPClient_GetName(t *testing.T) {\n\tec := HTTPClient{Name: \"my-http-action\"}\n\trequire.NoError(t, ec.Init())\n\trequire.Equal(t, \"my-http-action\", ec.GetName())\n}\n\nfunc TestHTTPClient_Send(t *testing.T) {\n\ttestCases := []struct {\n\t\tname           string\n\t\tmethod         string\n\t\tinputEvent     string\n\t\tbodyFile       string\n\t\tbodyContent    string\n\t\ttestServerFunc http.HandlerFunc\n\t\texpectedError  string\n\t}{\n\t\t{\n\t\t\tname:   \"happy path method get\",\n\t\t\tmethod: http.MethodGet,\n\t\t\ttestServerFunc: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equal(t, []string{\"bar\", \"baz\"}, r.Header.Values(\"fookey\"))\n\t\t\t\tassert.Empty(t, r.Header.Get(\"Postee-Event\")) // no event sent\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path method post with body file, string input event\",\n\t\t\tmethod:     http.MethodPost,\n\t\t\tbodyFile:   \"goldens/validbody.txt\",\n\t\t\tinputEvent: \"foo bar baz header\",\n\t\t\ttestServerFunc: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equal(t, []string{\"bar\", \"baz\"}, r.Header.Values(\"fookey\"))\n\t\t\t\tassert.Equal(t, \"Zm9vIGJhciBiYXogaGVhZGVy\", r.Header.Get(\"Postee-Event\"))\n\n\t\t\t\tb, _ := ioutil.ReadAll(r.Body)\n\t\t\t\tassert.Equal(t, \"foo bar baz body\", string(b))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:        \"happy path method post with body content, string input event\",\n\t\t\tmethod:      http.MethodPost,\n\t\t\tbodyContent: \"foo bar baz body\",\n\t\t\tinputEvent:  \"foo bar baz header\",\n\t\t\ttestServerFunc: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equal(t, []string{\"bar\", \"baz\"}, r.Header.Values(\"fookey\"))\n\t\t\t\tassert.Equal(t, \"Zm9vIGJhciBiYXogaGVhZGVy\", r.Header.Get(\"Postee-Event\"))\n\n\t\t\t\tb, _ := ioutil.ReadAll(r.Body)\n\t\t\t\tassert.Equal(t, \"foo bar baz body\", string(b))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:     \"happy path method post, json input event\",\n\t\t\tmethod:   http.MethodPost,\n\t\t\tbodyFile: \"goldens/validbody.txt\",\n\t\t\tinputEvent: `{\n\t\"argsNum\": 2\n}`,\n\t\t\ttestServerFunc: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equal(t, []string{\"bar\", \"baz\"}, r.Header.Values(\"fookey\"))\n\t\t\t\tassert.Equal(t, \"ewoJImFyZ3NOdW0iOiAyCn0=\", r.Header.Get(\"Postee-Event\"))\n\n\t\t\t\tb, _ := ioutil.ReadAll(r.Body)\n\t\t\t\tassert.Equal(t, \"foo bar baz body\", string(b))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"happy path method post, with relative body content, json input event\",\n\t\t\tmethod: http.MethodPost,\n\t\t\tbodyContent: `argsNum: event.input.argsNum\neventID: event.input.eventID`,\n\t\t\tinputEvent: `{\n\t\"argsNum\": 2,\n\t\"eventID\": \"TRC-2\"\n}`,\n\t\t\ttestServerFunc: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equal(t, []string{\"bar\", \"baz\"}, r.Header.Values(\"fookey\"))\n\t\t\t\tassert.Equal(t, \"ewoJImFyZ3NOdW0iOiAyLAoJImV2ZW50SUQiOiAiVFJDLTIiCn0=\", r.Header.Get(\"Postee-Event\"))\n\n\t\t\t\tb, _ := ioutil.ReadAll(r.Body)\n\t\t\t\tassert.Equal(t, `argsNum: 2\neventID: TRC-2`, string(b))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"happy path method post, with relative body content, string input event\",\n\t\t\tmethod: http.MethodPost,\n\t\t\tbodyContent: `event1: event.input\nevent1: event.input`,\n\t\t\tinputEvent: `\"argsNum\": 2, \"eventID\": \"TRC-2\"`,\n\t\t\ttestServerFunc: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tassert.Equal(t, []string{\"bar\", \"baz\"}, r.Header.Values(\"fookey\"))\n\t\t\t\tassert.Equal(t, \"ImFyZ3NOdW0iOiAyLCAiZXZlbnRJRCI6ICJUUkMtMiI=\", r.Header.Get(\"Postee-Event\"))\n\n\t\t\t\tb, _ := ioutil.ReadAll(r.Body)\n\t\t\t\tassert.Equal(t, `event1: \"argsNum\": 2, \"eventID\": \"TRC-2\"\nevent1: \"argsNum\": 2, \"eventID\": \"TRC-2\"`, string(b))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"sad path method get - server unavailable\",\n\t\t\tmethod: http.MethodGet,\n\t\t\ttestServerFunc: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\t_, _ = w.Write([]byte(\"internal server error\"))\n\t\t\t},\n\t\t\texpectedError: \"http status NOT OK: HTTP 500 Internal Server Error, response: internal server error\",\n\t\t},\n\t\t{\n\t\t\tname:          \"sad path method get - bad url\",\n\t\t\tmethod:        http.MethodGet,\n\t\t\texpectedError: `Get \"path-to-nowhere\": unsupported protocol scheme \"\"`,\n\t\t},\n\t\t{\n\t\t\tname:          \"sad path, body file not found\",\n\t\t\tmethod:        http.MethodPost,\n\t\t\tbodyFile:      \"invalid.txt\",\n\t\t\texpectedError: \"unable to read body file: invalid.txt, err: open invalid.txt: no such file or directory\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tvar testUrl *url.URL\n\t\t\tif tc.testServerFunc != nil {\n\t\t\t\tts := httptest.NewServer(tc.testServerFunc)\n\t\t\t\ttestUrl, _ = url.Parse(ts.URL)\n\t\t\t} else {\n\t\t\t\ttestUrl, _ = url.Parse(\"path-to-nowhere\")\n\t\t\t}\n\n\t\t\tec := HTTPClient{\n\t\t\t\tURL:         testUrl,\n\t\t\t\tMethod:      tc.method,\n\t\t\t\tHeaders:     map[string][]string{\"fookey\": {\"bar\", \"baz\"}},\n\t\t\t\tBodyFile:    tc.bodyFile,\n\t\t\t\tBodyContent: tc.bodyContent,\n\t\t\t}\n\n\t\t\tswitch {\n\t\t\tcase tc.expectedError != \"\":\n\t\t\t\trequire.EqualError(t, ec.Send(map[string]string{\"description\": \"foo bar baz header\"}), tc.expectedError, tc.name)\n\t\t\tdefault:\n\t\t\t\trequire.NoError(t, ec.Send(map[string]string{\"description\": tc.inputEvent}), tc.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "actions/jira.go",
    "content": "package actions\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"strconv\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/go-jira\"\n)\n\nconst (\n\tdefaultIssueType      = \"Task\"\n\tdefaultIssuePriority  = \"High\"\n\tdefaultSprintPlugin   = \"com.pyxis.greenhopper.jira:gh-sprint\"\n\tNotConfiguredSprintId = -1\n)\n\ntype JiraAPI struct {\n\tName            string\n\tUrl             string\n\tUser            string\n\tPassword        string\n\tToken           string\n\tTlsVerify       bool\n\tIssuetype       string\n\tProjectKey      string\n\tPriority        string\n\tAssignee        []string\n\tDescription     string\n\tSummary         string\n\tSprintName      string\n\tSprintId        int\n\tFixVersions     []string\n\tAffectsVersions []string\n\tLabels          []string\n\tUnknowns        map[string]string\n\tBoardName       string\n\tboardId         int\n\tboardType       string\n}\n\nfunc (ctx *JiraAPI) GetName() string {\n\treturn ctx.Name\n}\n\nfunc (ctx *JiraAPI) fetchBoardId(boardName string) {\n\tclient, err := createClient(ctx)\n\tif err != nil {\n\t\tlog.Printf(\"unable to create Jira client: %s, please check your credentials.\", err)\n\t\treturn\n\t}\n\n\tboardlist, _, err := client.Board.GetAllBoards(&jira.BoardListOptions{ProjectKeyOrID: ctx.ProjectKey})\n\tif err != nil {\n\t\tlog.Printf(\"failed to get boards from Jira API GetAllBoards with ProjectID %s. %s\", ctx.ProjectKey, err)\n\t\treturn\n\t}\n\tvar matches int\n\tfor _, board := range boardlist.Values {\n\t\tif board.Name == boardName {\n\t\t\tctx.boardId = board.ID\n\t\t\tctx.boardType = board.Type\n\t\t\tmatches++\n\t\t}\n\t}\n\n\tif matches > 1 {\n\t\tlog.Printf(\"found more than one boards with name %q, working with board id %d\", boardName, ctx.boardId)\n\t} else if matches == 0 {\n\t\tlog.Printf(\"no boards found with name %s when getting all boards for User\", boardName)\n\t\treturn\n\t} else {\n\t\tlog.Printf(\"using board ID %d with Name %q\", ctx.boardId, boardName)\n\t}\n}\n\nfunc (ctx *JiraAPI) fetchSprintId(client *jira.Client) {\n\tsprints, _, err := client.Board.GetAllSprintsWithOptions(ctx.boardId, &jira.GetAllSprintsOptions{State: \"active\"})\n\tif err != nil {\n\t\tlog.Printf(\"failed to get active sprint for board ID %d from Jira API. %s\", ctx.boardId, err)\n\t\treturn\n\t}\n\tif len(sprints.Values) > 1 {\n\t\tctx.SprintId = len(sprints.Values) - 1\n\t\tlog.Printf(\"Found more than one active sprint, using sprint id %d as the active sprint\", ctx.SprintId)\n\t} else if len(sprints.Values) == 1 {\n\t\tif sprints.Values[0].ID != ctx.SprintId {\n\t\t\tctx.SprintId = sprints.Values[0].ID\n\t\t\tlog.Printf(\"using sprint id %d as the active sprint\", ctx.SprintId)\n\t\t}\n\t} else {\n\t\tlog.Printf(\"no active sprints exist in board ID %d Name %s\", ctx.boardId, ctx.ProjectKey)\n\t}\n}\n\nfunc (ctx *JiraAPI) Terminate() error {\n\tlog.Printf(\"Jira action terminated\\n\")\n\treturn nil\n}\n\nfunc (ctx *JiraAPI) Init() error {\n\tif ctx.BoardName == \"\" {\n\t\tctx.BoardName = fmt.Sprintf(\"%s board\", ctx.ProjectKey)\n\t}\n\tctx.fetchBoardId(ctx.BoardName)\n\n\tlog.Printf(\"Starting Jira action %q....\", ctx.Name)\n\tif len(ctx.Password) == 0 {\n\t\tctx.Password = os.Getenv(\"JIRA_PASSWORD\")\n\t}\n\treturn nil\n}\n\nfunc (jira *JiraAPI) GetLayoutProvider() layout.LayoutProvider {\n\treturn new(formatting.JiraLayoutProvider)\n}\n\nfunc (ctx *JiraAPI) buildTransportClient() (*http.Client, error) {\n\tif ctx.Token != \"\" {\n\t\tif !isServerJira(ctx.Url) {\n\t\t\treturn nil, errors.New(\"Jira Cloud can't work with PAT\")\n\t\t}\n\t\tif ctx.Password != \"\" {\n\t\t\tlog.Printf(\"Found both Password and PAT, using PAT to authenticate.\")\n\t\t}\n\t\ttp := jira.BearerTokenAuthTransport{\n\t\t\tToken: ctx.Token,\n\t\t}\n\t\tif !ctx.TlsVerify {\n\t\t\ttp.Transport = &http.Transport{\n\t\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t}\n\t\t}\n\t\treturn tp.Client(), nil\n\t} else {\n\t\ttp := jira.BasicAuthTransport{\n\t\t\tUsername: ctx.User,\n\t\t\tPassword: ctx.Password,\n\t\t}\n\t\tif !ctx.TlsVerify {\n\t\t\ttp.Transport = &http.Transport{\n\t\t\t\tTLSClientConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t}\n\t\t}\n\t\treturn tp.Client(), nil\n\t}\n}\n\nvar createClient = func(ctx *JiraAPI) (*jira.Client, error) {\n\ttpClient, err := ctx.buildTransportClient()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"unable to create new JIRA client. %w\", err)\n\t}\n\tclient, err := jira.NewClient(tpClient, ctx.Url)\n\tif err != nil {\n\t\treturn client, fmt.Errorf(\"unable to create new JIRA client. %w\", err)\n\t}\n\treturn client, nil\n}\n\nfunc (ctx *JiraAPI) Send(content map[string]string) error {\n\tclient, err := createClient(ctx)\n\tif err != nil {\n\t\tlog.Printf(\"unable to create Jira client: %s\", err)\n\t\treturn err\n\t}\n\n\tif ctx.boardType == \"scrum\" {\n\t\tctx.fetchSprintId(client)\n\t}\n\n\tmetaProject, err := createMetaProject(client, ctx.ProjectKey)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to create meta project: %w\", err)\n\t}\n\n\tctx.Issuetype, err = getIssueType(ctx, metaProject)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to get issuetype: %w\", err)\n\t}\n\n\tmetaIssueType, err := createMetaIssueType(metaProject, ctx.Issuetype)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to create meta issue type: %w\", err)\n\t}\n\n\tctx.Summary = content[\"title\"]\n\tctx.Description = content[\"description\"]\n\n\ttype Version struct {\n\t\tName string `json:\"name\"`\n\t}\n\n\tfieldsConfig, err := createFieldsConfig(ctx, client, &content)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Failed to create fields config: %w\", err)\n\t}\n\n\tissue, err := InitIssue(client, metaProject, metaIssueType, fieldsConfig, isServerJira(ctx.Url))\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to init issue: %s\\n\", err)\n\t\treturn err\n\t}\n\n\tif len(ctx.Labels) > 0 {\n\t\tfor _, l := range ctx.Labels {\n\t\t\tissue.Fields.Labels = append(issue.Fields.Labels, l)\n\t\t}\n\t}\n\n\tif len(ctx.FixVersions) > 0 {\n\t\tfor _, v := range ctx.FixVersions {\n\t\t\tissue.Fields.FixVersions = append(issue.Fields.FixVersions, &jira.FixVersion{\n\t\t\t\tName: v,\n\t\t\t})\n\t\t}\n\t}\n\n\tif len(ctx.AffectsVersions) > 0 {\n\t\taffectsVersions := []*Version{}\n\t\tfor _, v := range ctx.AffectsVersions {\n\t\t\taffectsVersions = append(affectsVersions, &Version{\n\t\t\t\tName: v,\n\t\t\t})\n\t\t}\n\t\tissue.Fields.Unknowns[\"versions\"] = affectsVersions\n\t\tlog.Printf(\"added %d affected versions into Versions field\", len(ctx.AffectsVersions))\n\t}\n\n\ti, err := ctx.openIssue(client, issue)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to open jira issue, %s\\n\", err)\n\t\treturn err\n\t}\n\tlog.Printf(\"Created new jira issue %s\", i.ID)\n\treturn nil\n}\n\nfunc (ctx *JiraAPI) openIssue(client *jira.Client, issue *jira.Issue) (*jira.Issue, error) {\n\ti, res, err := client.Issue.Create(issue)\n\n\tdefer res.Body.Close()\n\tresp, _ := ioutil.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, errors.New(string(resp))\n\t}\n\treturn i, nil\n}\n\nfunc createMetaProject(c *jira.Client, project string) (*jira.MetaProject, error) {\n\tmeta, _, err := c.Issue.GetCreateMeta(project)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get create meta : %w\", err)\n\t}\n\n\t// get right project\n\tmetaProject := meta.GetProjectWithKey(project)\n\tif metaProject == nil {\n\t\treturn nil, fmt.Errorf(\"could not find project with key %s\", project)\n\t}\n\n\treturn metaProject, nil\n}\n\nfunc getIssueType(ctx *JiraAPI, metaProject *jira.MetaProject) (string, error) {\n\tif ctx.Issuetype != \"\" {\n\t\tif validateIssueType(ctx.Issuetype, metaProject) { // check IssueType from context\n\t\t\treturn ctx.Issuetype, nil\n\t\t} else {\n\t\t\treturn \"\", fmt.Errorf(\"project %q doesn't have issueType %q\", metaProject.Name, ctx.Issuetype)\n\t\t}\n\t} else {\n\t\tif validateIssueType(defaultIssueType, metaProject) { // check default Issue Type\n\t\t\treturn defaultIssueType, nil\n\t\t}\n\t\tif len(metaProject.IssueTypes) > 0 { // use 1st issueType from REST API\n\t\t\treturn metaProject.IssueTypes[0].Name, nil\n\t\t} else {\n\t\t\treturn \"\", fmt.Errorf(\"project %q doesn't have issueTypes\", metaProject.Name)\n\t\t}\n\t}\n}\n\nfunc validateIssueType(issueType string, metaProject *jira.MetaProject) bool {\n\tfor _, it := range metaProject.IssueTypes { // get issueTypes list from REST API\n\t\tif issueType == it.Name {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nvar getIssuePriority = func(ctx *JiraAPI, client *jira.Client) (string, error) {\n\tissuePriorityList, _, err := client.Priority.GetList()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif ctx.Priority != \"\" {\n\t\tif validateIssuePriority(ctx.Priority, issuePriorityList) { // check Priority from context\n\t\t\treturn ctx.Priority, nil\n\t\t} else {\n\t\t\treturn \"\", fmt.Errorf(\"project doesn't have issue priority %q\", ctx.Priority)\n\t\t}\n\t} else {\n\t\tif validateIssuePriority(defaultIssuePriority, issuePriorityList) { // check default Priority\n\t\t\treturn defaultIssuePriority, nil\n\t\t} else {\n\t\t\tif len(issuePriorityList) > 0 {\n\t\t\t\treturn issuePriorityList[0].Name, nil // use 1st priority from REST API\n\t\t\t} else {\n\t\t\t\treturn \"\", fmt.Errorf(\"project doesn't have issue priorities\")\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc validateIssuePriority(priority string, priorityList []jira.Priority) bool {\n\tfor _, p := range priorityList {\n\t\tif priority == p.Name {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc createFieldsConfig(ctx *JiraAPI, client *jira.Client, content *map[string]string) (map[string]string, error) {\n\tfields, _, err := client.Field.GetList()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tassignee := ctx.User\n\tif len(ctx.Assignee) > 0 {\n\t\tassignees := getHandledRecipients(ctx.Assignee, content, ctx.Name)\n\t\tif len(assignees) > 0 {\n\t\t\tassignee = assignees[0]\n\t\t}\n\t}\n\n\tctx.Priority, err = getIssuePriority(ctx, client)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get issue priority: %w\", err)\n\t}\n\n\tfieldsConfig := make(map[string]string)\n\n\tfor _, field := range fields {\n\t\tswitch field.ID {\n\t\tcase \"issuetype\":\n\t\t\tfieldsConfig[field.Name] = ctx.Issuetype\n\t\tcase \"project\":\n\t\t\tfieldsConfig[field.Name] = ctx.ProjectKey\n\t\tcase \"priority\":\n\t\t\tfieldsConfig[field.Name] = ctx.Priority\n\t\tcase \"assignee\":\n\t\t\tfieldsConfig[field.Name] = assignee\n\t\tcase \"description\":\n\t\t\tfieldsConfig[field.Name] = ctx.Description\n\t\tcase \"summary\":\n\t\t\tfieldsConfig[field.Name] = ctx.Summary\n\t\tdefault:\n\t\t\t// Sprint is jira custom field. We found field.Name for sprint by plugin name.\n\t\t\t// \"com.pyxis.greenhopper.jira:gh-sprint\" is custom field that come bundled with Jira.\n\t\t\t// https://support.atlassian.com/jira-cloud-administration/docs/import-data-from-json\n\t\t\tif ctx.SprintId > 0 && field.Schema.Custom == defaultSprintPlugin {\n\t\t\t\tfieldsConfig[field.Name] = strconv.Itoa(ctx.SprintId)\n\t\t\t}\n\t\t}\n\t}\n\n\t//Add all custom fields that are unknown to fieldsConfig. Unknown are fields that are custom User defined in jira.\n\tfor k, v := range ctx.Unknowns {\n\t\tfieldsConfig[k] = v\n\t}\n\tif len(ctx.Unknowns) > 0 {\n\t\tlog.Printf(\"added %d custom fields to issue.\", len(ctx.Unknowns))\n\t}\n\n\treturn fieldsConfig, nil\n}\n\nfunc createMetaIssueType(metaProject *jira.MetaProject, issueType string) (*jira.MetaIssueType, error) {\n\tmetaIssuetype := metaProject.GetIssueTypeWithName(issueType)\n\tif metaIssuetype == nil {\n\t\treturn nil, fmt.Errorf(\"could not find issuetype %s\", issueType)\n\t}\n\n\treturn metaIssuetype, nil\n}\n\nfunc InitIssue(c *jira.Client, metaProject *jira.MetaProject, metaIssuetype *jira.MetaIssueType, fieldsConfig map[string]string, useSrvApi bool) (*jira.Issue, error) {\n\tissue := new(jira.Issue)\n\tissueFields := new(jira.IssueFields)\n\tissueFields.Unknowns = make(map[string]interface{})\n\n\t// map the field names the User presented to jira's internal key\n\tallFields, _ := metaIssuetype.GetAllFields()\n\tfor key, value := range fieldsConfig {\n\n\t\tjiraKey, found := allFields[key]\n\t\tif !found {\n\t\t\treturn nil, fmt.Errorf(\"key %s is not found in the list of fields\", key)\n\t\t}\n\n\t\tvalueType, err := metaIssuetype.Fields.String(jiraKey + \"/schema/type\")\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tswitch strings.ToLower(valueType) {\n\t\tcase \"array\":\n\t\t\t// split value (string) into slice by delimiter\n\t\t\telements := strings.Split(value, \",\")\n\n\t\t\telemType, err := metaIssuetype.Fields.String(jiraKey + \"/schema/items\")\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tswitch elemType {\n\t\t\tcase \"component\":\n\t\t\t\tissueFields.Unknowns[jiraKey] = []jira.Component{{Name: value}}\n\t\t\tcase \"option\":\n\t\t\t\toptionsMap := make([]map[string]string, 0)\n\n\t\t\t\tfor _, element := range elements {\n\t\t\t\t\toptionsMap = append(optionsMap, map[string]string{\"value\": element})\n\t\t\t\t}\n\t\t\t\tissueFields.Unknowns[jiraKey] = optionsMap\n\t\t\tdefault:\n\t\t\t\tif key == \"Sprint\" {\n\t\t\t\t\tnum, err := strconv.Atoi(value)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"Failed convert 'Sprint' value(string) to int: %w\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\tissueFields.Unknowns[jiraKey] = num // Due to Jira REST API behavior, needed to specify not a slice but a number.\n\t\t\t\t} else {\n\t\t\t\t\tissueFields.Unknowns[jiraKey] = []string{value}\n\t\t\t\t}\n\t\t\t}\n\t\tcase \"number\":\n\t\t\tval, err := strconv.Atoi(value)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"Failed convert '%s' value(string) to int: %w\\n\", key, err)\n\t\t\t}\n\t\t\tissueFields.Unknowns[jiraKey] = val\n\n\t\t// TODO: Handle Cascading Select List\n\t\t//case \"option-with-child\":\n\t\t//\ttype CustomField struct {\n\t\t//\t\tValue string `json:\"value\"`\n\t\t//\t}\n\t\t//\ttype CustomFieldCascading struct {\n\t\t//\t\tValue string `json:\"value\"`\n\t\t//\t\tChild CustomField `json:\"child\"`\n\t\t//\t}\n\t\t//\n\t\t//\ta := CustomFieldCascading{ Value: \"1\", Child: CustomField{Value: \"a\"}}\n\n\t\tcase \"string\":\n\t\t\tissueFields.Unknowns[jiraKey] = value\n\t\tcase \"date\":\n\t\t\tissueFields.Unknowns[jiraKey] = value\n\t\tcase \"datetime\":\n\t\t\tissueFields.Unknowns[jiraKey] = value\n\t\tcase \"any\":\n\t\t\t// Treat any as string\n\t\t\tissueFields.Unknowns[jiraKey] = value\n\t\tcase \"project\":\n\t\t\tissueFields.Unknowns[jiraKey] = jira.Project{\n\t\t\t\tName: metaProject.Name,\n\t\t\t\tID:   metaProject.Id,\n\t\t\t}\n\t\tcase \"priority\":\n\t\t\tissueFields.Unknowns[jiraKey] = jira.Priority{Name: value}\n\t\tcase \"user\":\n\t\t\tvar users []jira.User\n\t\t\tvar resp *jira.Response\n\t\t\tvar err error\n\n\t\t\tif useSrvApi {\n\t\t\t\tusers, resp, err = findUserOnJiraServer(c, value)\n\t\t\t} else {\n\t\t\t\tusers, resp, err = c.User.Find(value)\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Get Jira User info error: %v\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusOK {\n\t\t\t\tlog.Printf(\"http response failed: %q\", resp.Status)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif len(users) == 0 {\n\t\t\t\tlog.Printf(\"There is no user for %q\", value)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tissueFields.Unknowns[jiraKey] = users[0]\n\t\tcase \"issuetype\":\n\t\t\tissueFields.Unknowns[jiraKey] = jira.IssueType{\n\t\t\t\tName: value,\n\t\t\t}\n\t\tcase \"option\":\n\t\t\tissueFields.Unknowns[jiraKey] = jira.Option{\n\t\t\t\tValue: value,\n\t\t\t}\n\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"Unknown issue type encountered: %s for %s\", valueType, key)\n\t\t}\n\t}\n\tissue.Fields = issueFields\n\treturn issue, nil\n}\nfunc findUserOnJiraServer(c *jira.Client, email string) ([]jira.User, *jira.Response, error) {\n\treq, _ := c.NewRequest(\"GET\", fmt.Sprintf(\"/rest/api/2/user/search?username=%s\", email), nil)\n\n\tusers := []jira.User{}\n\n\tresp, err := c.Do(req, &users)\n\tif err != nil {\n\t\tlog.Printf(\"%v\", err)\n\t\treturn nil, resp, err\n\t}\n\treturn users, resp, nil\n}\nfunc isServerJira(rawUrl string) bool {\n\tjiraUrl, err := url.Parse(rawUrl)\n\n\tif err == nil {\n\t\treturn !strings.HasSuffix(jiraUrl.Host, \"atlassian.net\")\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "actions/jira_test.go",
    "content": "package actions\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/go-jira\"\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar metaIssuetype = &jira.MetaIssueType{Name: \"Task\", Fields: map[string]interface{}{\n\t\"issuetype\": map[string]interface{}{\n\t\t\"name\": \"Issue Type\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"issuetype\",\n\t\t},\n\t},\n\t\"project\": map[string]interface{}{\n\t\t\"name\": \"Project\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"project\",\n\t\t},\n\t},\n\t\"priority\": map[string]interface{}{\n\t\t\"name\": \"Priority\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"priority\",\n\t\t},\n\t},\n\t\"description\": map[string]interface{}{\n\t\t\"name\": \"Description\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"string\",\n\t\t},\n\t},\n\t\"summary\": map[string]interface{}{\n\t\t\"name\": \"Summary\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"string\",\n\t\t},\n\t},\n\t\"assignee\": map[string]interface{}{\n\t\t\"name\": \"Assignee\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"user\",\n\t\t},\n\t},\n\t\"customfield_10020\": map[string]interface{}{\n\t\t\"name\": \"Sprint\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\":  \"array\",\n\t\t\t\"items\": \"json\",\n\t\t},\n\t},\n\t\"customfield_10021\": map[string]interface{}{\n\t\t\"name\": \"Flagged\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\":  \"array\",\n\t\t\t\"items\": \"option\",\n\t\t},\n\t},\n\t\"components\": map[string]interface{}{\n\t\t\"name\": \"Components\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\":  \"array\",\n\t\t\t\"items\": \"component\",\n\t\t},\n\t},\n\t\"versions\": map[string]interface{}{\n\t\t\"name\": \"Affects versions\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\":  \"array\",\n\t\t\t\"items\": \"version\",\n\t\t},\n\t},\n\t\"customfield_10015\": map[string]interface{}{\n\t\t\"name\": \"Start date\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"date\",\n\t\t},\n\t},\n\t\"customfield_10009\": map[string]interface{}{\n\t\t\"name\": \"Actual end\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"datetime\",\n\t\t},\n\t},\n\t\"customfield_10001\": map[string]interface{}{\n\t\t\"name\": \"Team\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"any\",\n\t\t},\n\t},\n\t\"customfield_10004\": map[string]interface{}{\n\t\t\"name\": \"Impact\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"option\",\n\t\t},\n\t},\n\t\"timespent\": map[string]interface{}{\n\t\t\"name\": \"Time Spent\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"number\",\n\t\t},\n\t},\n\t\"customfield_10052\": map[string]interface{}{\n\t\t\"name\":   \"No schema type\",\n\t\t\"schema\": map[string]interface{}{},\n\t},\n\t\"customfield_10053\": map[string]interface{}{\n\t\t\"name\": \"No schema items\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"array\",\n\t\t},\n\t},\n\t\"customfield_10054\": map[string]interface{}{\n\t\t\"name\": \"Bad Type\",\n\t\t\"schema\": map[string]interface{}{\n\t\t\t\"type\": \"badType\",\n\t\t},\n\t},\n}}\n\nvar fieldList = &[]jira.Field{\n\t{ID: \"issuetype\", Name: \"Issue Type\"},\n\t{ID: \"project\", Name: \"Project\"},\n\t{ID: \"priority\", Name: \"Priority\"},\n\t{ID: \"description\", Name: \"Description\"},\n\t{ID: \"summary\", Name: \"Summary\"},\n}\n\nfunc TestJiraAPI_GetName(t *testing.T) {\n\tt.Run(\"happy path\", func(t *testing.T) {\n\t\tjiraApi := &JiraAPI{Name: \"testName\"}\n\t\tname := jiraApi.GetName()\n\n\t\tassert.Equal(t, jiraApi.Name, name)\n\t})\n}\n\nfunc TestJiraAPI_FetchBoardId(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tboardName   string\n\t\tboardList   *jira.BoardsList\n\t\twantJiraApi *JiraAPI\n\t\twantError   string\n\t}{\n\t\t{\n\t\t\tname:        \"happy path (0 boards found)\",\n\t\t\tboardName:   \"board0\",\n\t\t\tboardList:   &jira.BoardsList{Values: []jira.Board{{Name: \"board1\"}, {Name: \"board2\"}}},\n\t\t\twantJiraApi: &JiraAPI{BoardName: \"board0\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"happy path (1 board found)\",\n\t\t\tboardName:   \"board1\",\n\t\t\tboardList:   &jira.BoardsList{Values: []jira.Board{{Name: \"board1\", ID: 1, Type: \"Scrum\"}, {Name: \"board2\", ID: 2, Type: \"Scrum\"}}},\n\t\t\twantJiraApi: &JiraAPI{boardId: 1, BoardName: \"board1\", boardType: \"Scrum\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"happy path (2 boards found)\",\n\t\t\tboardName:   \"board2\",\n\t\t\tboardList:   &jira.BoardsList{Values: []jira.Board{{Name: \"board2\", ID: 1, Type: \"Scrum\"}, {Name: \"board2\", ID: 2, Type: \"Scrum\"}}},\n\t\t\twantJiraApi: &JiraAPI{boardId: 2, BoardName: \"board2\", boardType: \"Scrum\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"sad path (Failed to create client)\",\n\t\t\tboardName:   \"board3\",\n\t\t\twantJiraApi: &JiraAPI{BoardName: \"board3\"},\n\t\t\twantError:   \"Failed to create client\",\n\t\t},\n\t\t{\n\t\t\tname:        \"sad path (Failed to get boardList)\",\n\t\t\tboardName:   \"board4\",\n\t\t\twantJiraApi: &JiraAPI{BoardName: \"board4\"},\n\t\t\twantError:   \"Failed to get boardList\",\n\t\t},\n\t}\n\n\toldCreateClient := createClient\n\tdefer func() { createClient = oldCreateClient }()\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.boardList, test.wantError)))\n\t\t\tdefer ts.Close()\n\n\t\t\tcreateClient = func(ctx *JiraAPI) (*jira.Client, error) {\n\t\t\t\tif test.wantError == \"Failed to create client\" {\n\t\t\t\t\treturn nil, fmt.Errorf(test.wantError)\n\t\t\t\t} else {\n\t\t\t\t\treturn jira.NewClient(ts.Client(), ts.URL)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tjiraApi := &JiraAPI{BoardName: test.boardName}\n\t\t\tjiraApi.fetchBoardId(test.boardName)\n\n\t\t\tassert.Equal(t, test.wantJiraApi, jiraApi)\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_FetchSprintId(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tsprints      *jira.SprintsList\n\t\twantSprintId int\n\t}{\n\t\t{\n\t\t\tname:         \"happy path (2 sprints found)\",\n\t\t\tsprints:      &jira.SprintsList{Values: []jira.Sprint{{Name: \"sprint0\"}, {Name: \"sprint1\"}}},\n\t\t\twantSprintId: 1,\n\t\t},\n\t\t{\n\t\t\tname:         \"happy path (1 sprint found)\",\n\t\t\tsprints:      &jira.SprintsList{Values: []jira.Sprint{{Name: \"sprint32\", ID: 32}}},\n\t\t\twantSprintId: 32,\n\t\t},\n\t\t{\n\t\t\tname:         \"happy path (0 sprints found)\",\n\t\t\tsprints:      &jira.SprintsList{Values: []jira.Sprint{}},\n\t\t\twantSprintId: NotConfiguredSprintId,\n\t\t},\n\t\t{\n\t\t\tname:         \"sad path (Failed to get all sprints)\",\n\t\t\twantSprintId: NotConfiguredSprintId,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.sprints, \"\")))\n\t\t\tdefer ts.Close()\n\n\t\t\tjiraApi := &JiraAPI{SprintId: -1}\n\t\t\tclient, err := jira.NewClient(ts.Client(), ts.URL)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"can't create jiraClient %v\", err)\n\t\t\t}\n\n\t\t\tjiraApi.fetchSprintId(client)\n\n\t\t\tassert.Equal(t, test.wantSprintId, jiraApi.SprintId)\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_InitIssue(t *testing.T) {\n\tmetaProject := &jira.MetaProject{\n\t\tId:   \"project ID\",\n\t\tName: \"project name\",\n\t}\n\ttests := []struct {\n\t\tname            string\n\t\tuseSrvApi       bool\n\t\thttpStatus      int\n\t\tuser            []*jira.User\n\t\tfieldsConfig    map[string]string\n\t\twantIssueFields *jira.IssueFields\n\t\twantError       string\n\t}{\n\t\t{\n\t\t\tname:       \"happy path\",\n\t\t\tuseSrvApi:  true,\n\t\t\thttpStatus: http.StatusOK,\n\t\t\tuser:       []*jira.User{{Name: \"User\"}},\n\t\t\tfieldsConfig: map[string]string{\n\t\t\t\t\"Issue Type\":       \"Task\",\n\t\t\t\t\"Project\":          \"Project\",\n\t\t\t\t\"Priority\":         \"High\",\n\t\t\t\t\"Description\":      \"Description\",\n\t\t\t\t\"Summary\":          \"Summary\",\n\t\t\t\t\"Assignee\":         \"Assignee\",\n\t\t\t\t\"Sprint\":           \"1\",\n\t\t\t\t\"Flagged\":          \"Flagged\",\n\t\t\t\t\"Components\":       \"Components\",\n\t\t\t\t\"Affects versions\": \"1.0.1\",\n\t\t\t\t\"Start date\":       \"01.01.2022\",\n\t\t\t\t\"Actual end\":       \"01.01.2222\",\n\t\t\t\t\"Team\":             \"Team\",\n\t\t\t\t\"Impact\":           \"Impact\",\n\t\t\t\t\"Time Spent\":       \"10\",\n\t\t\t},\n\t\t\twantIssueFields: &jira.IssueFields{Unknowns: map[string]interface{}{\n\t\t\t\t\"issuetype\": jira.IssueType{Name: \"Task\"},\n\t\t\t\t\"project\": jira.Project{\n\t\t\t\t\tName: \"project name\",\n\t\t\t\t\tID:   \"project ID\",\n\t\t\t\t},\n\t\t\t\t\"priority\":          jira.Priority{Name: \"High\"},\n\t\t\t\t\"assignee\":          jira.User{Name: \"User\"},\n\t\t\t\t\"description\":       \"Description\",\n\t\t\t\t\"summary\":           \"Summary\",\n\t\t\t\t\"customfield_10020\": 1,\n\t\t\t\t\"customfield_10021\": []map[string]string{{\"value\": \"Flagged\"}},\n\t\t\t\t\"components\":        []jira.Component{{Name: \"Components\"}},\n\t\t\t\t\"versions\":          []string{\"1.0.1\"},\n\t\t\t\t\"customfield_10015\": \"01.01.2022\",\n\t\t\t\t\"customfield_10009\": \"01.01.2222\",\n\t\t\t\t\"customfield_10001\": \"Team\",\n\t\t\t\t\"customfield_10004\": jira.Option{Value: \"Impact\"},\n\t\t\t\t\"timespent\":         10,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path (useSrvApi = false)\",\n\t\t\tuseSrvApi:  false,\n\t\t\thttpStatus: http.StatusOK,\n\t\t\tuser:       []*jira.User{{Name: \"User\"}},\n\t\t\tfieldsConfig: map[string]string{\n\t\t\t\t\"Assignee\": \"Assignee\",\n\t\t\t},\n\t\t\twantIssueFields: &jira.IssueFields{Unknowns: map[string]interface{}{\n\t\t\t\t\"assignee\": jira.User{Name: \"User\"},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path (find user returns error)\",\n\t\t\thttpStatus: http.StatusOK,\n\t\t\tfieldsConfig: map[string]string{\n\t\t\t\t\"Assignee\": \"Assignee\",\n\t\t\t},\n\t\t\twantIssueFields: &jira.IssueFields{Unknowns: map[string]interface{}{}},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path (users not found)\",\n\t\t\thttpStatus: http.StatusOK,\n\t\t\tuser:       []*jira.User{},\n\t\t\tfieldsConfig: map[string]string{\n\t\t\t\t\"Assignee\": \"Assignee\",\n\t\t\t},\n\t\t\twantIssueFields: &jira.IssueFields{Unknowns: map[string]interface{}{}},\n\t\t},\n\t\t{\n\t\t\tname:         \"sad path (bad field in fieldsConfig)\",\n\t\t\tfieldsConfig: map[string]string{\"Bad-field\": \"bad-field\"},\n\t\t\twantError:    \"key Bad-field is not found in the list of fields\",\n\t\t},\n\t\t{\n\t\t\tname:         \"sad path (field doesn't have schema/type)\",\n\t\t\tfieldsConfig: map[string]string{\"No schema type\": \"No schema type\"},\n\t\t\twantError:    \"\\\"customfield_10052/schema/type\\\" is not set\",\n\t\t},\n\t\t{\n\t\t\tname:         \"sad path (field doesn't have schema/items)\",\n\t\t\tfieldsConfig: map[string]string{\"No schema items\": \"No schema items\"},\n\t\t\twantError:    \"\\\"customfield_10053/schema/items\\\" is not set\",\n\t\t},\n\t\t{\n\t\t\tname:         \"sad path (sprint is not a number)\",\n\t\t\tfieldsConfig: map[string]string{\"Sprint\": \"one\"},\n\t\t\twantError:    \"strconv.Atoi: parsing \\\"one\\\": invalid syntax\",\n\t\t},\n\t\t{\n\t\t\tname:         \"sad path (number field is not a number)\",\n\t\t\tfieldsConfig: map[string]string{\"Time Spent\": \"two\"},\n\t\t\twantError:    \"strconv.Atoi: parsing \\\"two\\\": invalid syntax\",\n\t\t},\n\t\t{\n\t\t\tname:         \"sad path (bad field type)\",\n\t\t\tfieldsConfig: map[string]string{\"Bad Type\": \"Bad Type\"},\n\t\t\twantError:    \"Unknown issue type encountered\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.user, test.wantError)))\n\t\t\tdefer ts.Close()\n\n\t\t\tjiraClient, err := jira.NewClient(ts.Client(), ts.URL)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"can't create jiraClient %v\", err)\n\t\t\t}\n\n\t\t\tissue, err := InitIssue(jiraClient, metaProject, metaIssuetype, test.fieldsConfig, test.useSrvApi)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, test.wantIssueFields, issue.Fields)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_GetLayoutProvider(t *testing.T) {\n\tt.Run(\"happy path\", func(t *testing.T) {\n\t\tjiraApi := &JiraAPI{}\n\t\twantLayoutProviderType := new(formatting.JiraLayoutProvider)\n\t\tLayoutProvider := jiraApi.GetLayoutProvider()\n\t\tassert.Equal(t, reflect.TypeOf(wantLayoutProviderType), reflect.TypeOf(LayoutProvider))\n\t})\n}\n\nfunc TestJiraAPI_BuildTransportClient(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tjiraApi       *JiraAPI\n\t\twantTransport interface{}\n\t\twantError     string\n\t}{\n\t\t{\n\t\t\tname:          \"happy path bearer auth\",\n\t\t\tjiraApi:       &JiraAPI{Token: \"token\", Password: \"password\"},\n\t\t\twantTransport: &jira.BearerTokenAuthTransport{},\n\t\t},\n\t\t{\n\t\t\tname:          \"happy path bearer auth\",\n\t\t\tjiraApi:       &JiraAPI{User: \"User\", Password: \"Password\"},\n\t\t\twantTransport: &jira.BasicAuthTransport{},\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path bearer auth for server jira\",\n\t\t\tjiraApi:   &JiraAPI{Token: \"token\", Url: \"https://johndoe.atlassian.net\"},\n\t\t\twantError: \"Jira Cloud can't work with PAT\",\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path bearer auth with bad url\",\n\t\t\tjiraApi:   &JiraAPI{Token: \"token\", Url: \"https://  johndoe.atlassian.net\"},\n\t\t\twantError: \"Jira Cloud can't work with PAT\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tclient, err := test.jiraApi.buildTransportClient()\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, reflect.TypeOf(test.wantTransport), reflect.TypeOf(client.Transport))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraApi_createClient(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tjiraApi   *JiraAPI\n\t\twantError string\n\t}{\n\t\t{\n\t\t\tname:    \"happy path\",\n\t\t\tjiraApi: &JiraAPI{},\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path (using PAT for cloud jira)\",\n\t\t\tjiraApi:   &JiraAPI{Token: \"token\", Url: \"https://johndoe.atlassian.net\"},\n\t\t\twantError: \"Jira Cloud can't work with PAT\",\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path (bad url)\",\n\t\t\tjiraApi:   &JiraAPI{Url: \"https://johndoe   .atlassian.net\"},\n\t\t\twantError: \"unable to create new JIRA client\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\t_, err := createClient(test.jiraApi)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\trequire.Nil(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_Send(t *testing.T) {\n\ttests := []struct {\n\t\tname           string\n\t\tjiraApi        *JiraAPI\n\t\tcreateMetaInfo *jira.CreateMetaInfo\n\t\tfieldList      *[]jira.Field\n\t\tpriorityList   *[]jira.Priority\n\t\tissue          *jira.Issue\n\t\tserverInfo     *jira.JiraServerInfo\n\t\tcontent        map[string]string\n\t\twantError      string\n\t}{\n\t\t{\n\t\t\tname: \"happy path\",\n\t\t\tjiraApi: &JiraAPI{\n\t\t\t\tProjectKey:      \"project\",\n\t\t\t\tUser:            \"user\",\n\t\t\t\tboardType:       \"scrum\",\n\t\t\t\tLabels:          []string{\"label1\", \"label2\"},\n\t\t\t\tFixVersions:     []string{\"fix1\", \"fix2\"},\n\t\t\t\tAffectsVersions: []string{\"affect1\", \"affect2\"},\n\t\t\t},\n\t\t\tcreateMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: \"project\", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}},\n\t\t\tfieldList:      fieldList,\n\t\t\tpriorityList:   &[]jira.Priority{{Name: \"High\"}},\n\t\t\tissue:          &jira.Issue{},\n\t\t\tserverInfo:     &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}},\n\t\t\tcontent:        map[string]string{\"title\": \"title_content\", \"description\": \"description_content\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path (Failed to create client)\",\n\t\t\twantError: \"Failed to create client\",\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path (Failed to create meta project)\",\n\t\t\tjiraApi:   &JiraAPI{},\n\t\t\twantError: \"Failed to create meta project\",\n\t\t},\n\t\t{\n\t\t\tname:           \"sad path (Failed to get issuetype)\",\n\t\t\tjiraApi:        &JiraAPI{Issuetype: \"bogusIssueType\", ProjectKey: \"project\"},\n\t\t\tserverInfo:     &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}},\n\t\t\tcreateMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: \"project\", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}},\n\t\t\twantError:      \"Failed to get issuetype\",\n\t\t},\n\t\t{\n\t\t\tname:           \"sad path (Failed to create fields config)\",\n\t\t\tjiraApi:        &JiraAPI{ProjectKey: \"project\"},\n\t\t\tserverInfo:     &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}},\n\t\t\tcreateMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: \"project\", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}},\n\t\t\twantError:      \"Failed to create fields config\",\n\t\t},\n\t\t{\n\t\t\tname:           \"sad path (Failed to init issue)\",\n\t\t\tjiraApi:        &JiraAPI{ProjectKey: \"project\", Unknowns: map[string]string{\"bad field\": \"bad field\"}},\n\t\t\tserverInfo:     &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}},\n\t\t\tcreateMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: \"project\", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}},\n\t\t\tfieldList:      fieldList,\n\t\t\tpriorityList:   &[]jira.Priority{{Name: \"High\"}},\n\t\t\twantError:      \"key bad field is not found in the list of fields\",\n\t\t},\n\t\t{\n\t\t\tname:           \"sad path (Failed to open issue)\",\n\t\t\tjiraApi:        &JiraAPI{ProjectKey: \"project\"},\n\t\t\tserverInfo:     &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}},\n\t\t\tcreateMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: \"project\", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}},\n\t\t\tfieldList:      fieldList,\n\t\t\tpriorityList:   &[]jira.Priority{{Name: \"High\"}},\n\t\t\twantError:      \"Failed to open issue\",\n\t\t},\n\t}\n\n\toldCreateClient := createClient\n\tdefer func() { createClient = oldCreateClient }()\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmux := http.NewServeMux()\n\t\t\tmux.HandleFunc(\"/rest/api/2/issue/createmeta/\", buildHttpHandler(test.createMetaInfo, test.wantError))\n\t\t\tmux.HandleFunc(\"/rest/api/2/field\", buildHttpHandler(test.fieldList, test.wantError))\n\t\t\tmux.HandleFunc(\"/rest/api/2/priority\", buildHttpHandler(test.priorityList, test.wantError))\n\t\t\tmux.HandleFunc(\"/rest/api/2/issue\", buildHttpHandler(test.issue, test.wantError))\n\t\t\tmux.HandleFunc(\"/rest/api/2/serverInfo\", buildHttpHandler(test.serverInfo, test.wantError))\n\t\t\tts := httptest.NewServer(mux)\n\t\t\tdefer ts.Close()\n\n\t\t\tcreateClient = func(ctx *JiraAPI) (*jira.Client, error) {\n\t\t\t\tif test.wantError == \"Failed to create client\" {\n\t\t\t\t\treturn nil, fmt.Errorf(test.wantError)\n\t\t\t\t} else {\n\t\t\t\t\treturn jira.NewClient(ts.Client(), ts.URL)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr := test.jiraApi.Send(test.content)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_OpenIssue(t *testing.T) {\n\ttests := []struct {\n\t\tname      string\n\t\tissue     *jira.Issue\n\t\twantError string\n\t}{\n\t\t{\n\t\t\tname:  \"Happy path\",\n\t\t\tissue: &jira.Issue{ID: \"issue1\"},\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path\",\n\t\t\twantError: \"open issue error\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.issue, test.wantError)))\n\t\t\tdefer ts.Close()\n\n\t\t\tjiraApi := &JiraAPI{}\n\t\t\tjiraClient, err := jira.NewClient(ts.Client(), ts.URL)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"can't create jiraClient %v\", err)\n\t\t\t}\n\n\t\t\tissue, err := jiraApi.openIssue(jiraClient, test.issue)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, test.issue, issue)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_CreateMetaProject(t *testing.T) {\n\ttests := []struct {\n\t\tname               string\n\t\tmetaInfo           *jira.CreateMetaInfo\n\t\twantMetaProjectKey string\n\t\twantError          string\n\t}{\n\t\t{\n\t\t\tname: \"happy path\",\n\t\t\tmetaInfo: &jira.CreateMetaInfo{\n\t\t\t\tProjects: []*jira.MetaProject{\n\t\t\t\t\t{Key: \"test\"},\n\t\t\t\t\t{Key: \"debug\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantMetaProjectKey: \"debug\",\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path (jira return error)\",\n\t\t\twantError: \"failed to get create meta\",\n\t\t},\n\t\t{\n\t\t\tname: \"sad path (project not found)\",\n\t\t\tmetaInfo: &jira.CreateMetaInfo{\n\t\t\t\tProjects: []*jira.MetaProject{\n\t\t\t\t\t{Key: \"test\"},\n\t\t\t\t\t{Key: \"debug\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantMetaProjectKey: \"non-existent-project\",\n\t\t\twantError:          \"could not find project with key non-existent-project\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmux := http.NewServeMux()\n\t\t\tmux.HandleFunc(\"/rest/api/2/issue/createmeta/\", buildHttpHandler(test.metaInfo, test.wantError))\n\t\t\tmux.HandleFunc(\"/rest/api/2/serverInfo\", buildHttpHandler(&jira.JiraServerInfo{VersionNumbers: []int{8, 4, 0}}, test.wantError))\n\t\t\tts := httptest.NewServer(mux)\n\t\t\tdefer ts.Close()\n\n\t\t\tjiraClient, err := jira.NewClient(ts.Client(), ts.URL)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"can't create jiraClient %v\", err)\n\t\t\t}\n\n\t\t\tmetaProject, err := createMetaProject(jiraClient, test.wantMetaProjectKey)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, test.wantMetaProjectKey, metaProject.Key)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_CreateIssueType(t *testing.T) {\n\ttests := []struct {\n\t\tname          string\n\t\tjiraAPI       *JiraAPI\n\t\tmetaProject   *jira.MetaProject\n\t\twantIssueType string\n\t\twantError     string\n\t}{\n\t\t{\n\t\t\tname:          \"happy path (empty issueType, jira has 'Task' field)\",\n\t\t\tjiraAPI:       &JiraAPI{},\n\t\t\tmetaProject:   &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: \"Task\"}, {Name: \"Bug\"}}},\n\t\t\twantIssueType: \"Task\",\n\t\t},\n\t\t{\n\t\t\tname:          \"happy path (empty issueType, jira doesn't have 'Task' field)\",\n\t\t\tjiraAPI:       &JiraAPI{},\n\t\t\tmetaProject:   &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: \"Story\"}, {Name: \"Bug\"}}},\n\t\t\twantIssueType: \"Story\",\n\t\t},\n\t\t{\n\t\t\tname:          \"happy path (fill issueType, jira has 'Bug' field)\",\n\t\t\tjiraAPI:       &JiraAPI{Issuetype: \"Bug\"},\n\t\t\tmetaProject:   &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: \"Task\"}, {Name: \"Bug\"}}},\n\t\t\twantIssueType: \"Bug\",\n\t\t},\n\t\t{\n\t\t\tname:        \"bad path (fill issueType, jira doesn't have 'Bug' field)\",\n\t\t\tjiraAPI:     &JiraAPI{Issuetype: \"Bug\"},\n\t\t\tmetaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: \"Task\"}, {Name: \"Story\"}}},\n\t\t\twantError:   \"project \\\"\\\" doesn't have issueType \\\"Bug\\\"\",\n\t\t},\n\t\t{\n\t\t\tname:        \"bad path (metaIssueType has empty IssueTypes)\",\n\t\t\tjiraAPI:     &JiraAPI{Priority: \"\"},\n\t\t\tmetaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{}},\n\t\t\twantError:   \"project \\\"\\\" doesn't have issueTypes\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tissueType, err := getIssueType(test.jiraAPI, test.metaProject)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, test.wantIssueType, issueType)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_GetIssuePriority(t *testing.T) {\n\ttests := []struct {\n\t\tname         string\n\t\tjiraAPI      *JiraAPI\n\t\tpriorities   []*jira.Priority\n\t\twantPriority string\n\t\twantError    string\n\t}{\n\t\t{\n\t\t\tname:         \"happy path (empty priority, jira has 'High' field)\",\n\t\t\tjiraAPI:      &JiraAPI{Priority: \"\"},\n\t\t\tpriorities:   []*jira.Priority{{Name: \"Highest\"}, {Name: \"High\"}, {Name: \"Medium\"}},\n\t\t\twantPriority: \"High\",\n\t\t},\n\t\t{\n\t\t\tname:         \"happy path (empty priority, jira doesn't have 'High' field)\",\n\t\t\tjiraAPI:      &JiraAPI{Priority: \"\"},\n\t\t\tpriorities:   []*jira.Priority{{Name: \"Highest\"}, {Name: \"Low\"}, {Name: \"Medium\"}},\n\t\t\twantPriority: \"Highest\",\n\t\t},\n\t\t{\n\t\t\tname:         \"happy path (fill priority, jira has 'Medium' field)\",\n\t\t\tjiraAPI:      &JiraAPI{Priority: \"Medium\"},\n\t\t\tpriorities:   []*jira.Priority{{Name: \"Highest\"}, {Name: \"High\"}, {Name: \"Medium\"}},\n\t\t\twantPriority: \"Medium\",\n\t\t},\n\t\t{\n\t\t\tname:       \"bad path (fill priority, jira doesn't have 'Medium' field)\",\n\t\t\tjiraAPI:    &JiraAPI{Priority: \"Medium\"},\n\t\t\tpriorities: []*jira.Priority{{Name: \"Highest\"}, {Name: \"High\"}, {Name: \"Low\"}},\n\t\t\twantError:  \"project doesn't have issue priority \\\"Medium\\\"\",\n\t\t},\n\t\t{\n\t\t\tname:       \"bad path (jira returns empty priorities)\",\n\t\t\tjiraAPI:    &JiraAPI{Priority: \"\"},\n\t\t\tpriorities: []*jira.Priority{},\n\t\t\twantError:  \"project doesn't have issue priorities\",\n\t\t},\n\t\t{\n\t\t\tname:      \"bad path (jira returns error)\",\n\t\t\tjiraAPI:   &JiraAPI{Priority: \"\"},\n\t\t\twantError: \"json: cannot unmarshal object into Go value of type []jira.Priority\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.priorities, test.wantError)))\n\t\t\tdefer ts.Close()\n\n\t\t\tjiraClient, err := jira.NewClient(ts.Client(), ts.URL)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"can't create jiraClient %v\", err)\n\t\t\t}\n\n\t\t\tpriority, err := getIssuePriority(test.jiraAPI, jiraClient)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, test.wantPriority, priority)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_CreateFieldsConfig(t *testing.T) {\n\ttests := []struct {\n\t\tname             string\n\t\tfields           []*jira.Field\n\t\tjiraApi          *JiraAPI\n\t\tcontent          *map[string]string\n\t\twantFieldsConfig map[string]string\n\t\twantError        string\n\t}{\n\t\t{\n\t\t\tname: \"happy path (default field names)\",\n\t\t\tfields: []*jira.Field{\n\t\t\t\t{ID: \"issuetype\", Name: \"Issue Type\"},\n\t\t\t\t{ID: \"project\", Name: \"Project\"},\n\t\t\t\t{ID: \"priority\", Name: \"Priority\"},\n\t\t\t\t{ID: \"assignee\", Name: \"Assignee\"},\n\t\t\t\t{ID: \"description\", Name: \"Description\"},\n\t\t\t\t{ID: \"summary\", Name: \"Summary\"},\n\t\t\t},\n\t\t\tjiraApi: &JiraAPI{\n\t\t\t\tUser:        \"User\",\n\t\t\t\tIssuetype:   \"Task\",\n\t\t\t\tProjectKey:  \"Project\",\n\t\t\t\tPriority:    \"High\",\n\t\t\t\tDescription: \"Description\",\n\t\t\t\tSummary:     \"Summary\",\n\t\t\t},\n\t\t\tcontent: &map[string]string{},\n\t\t\twantFieldsConfig: map[string]string{\n\t\t\t\t\"Issue Type\":  \"Task\",\n\t\t\t\t\"Project\":     \"Project\",\n\t\t\t\t\"Priority\":    \"High\",\n\t\t\t\t\"Assignee\":    \"User\",\n\t\t\t\t\"Description\": \"Description\",\n\t\t\t\t\"Summary\":     \"Summary\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"happy path (custom field names)\",\n\t\t\tfields: []*jira.Field{\n\t\t\t\t{ID: \"issuetype\", Name: \"Custom Issue Type\"},\n\t\t\t\t{ID: \"project\", Name: \"Custom Project\"},\n\t\t\t\t{ID: \"priority\", Name: \"Custom Priority\"},\n\t\t\t\t{ID: \"assignee\", Name: \"Custom Assignee\"},\n\t\t\t\t{ID: \"description\", Name: \"Custom Description\"},\n\t\t\t\t{ID: \"summary\", Name: \"Custom Summary\"},\n\t\t\t\t{ID: \"customfield_10020\", Name: \"Custom Sprint\", Schema: jira.FieldSchema{Custom: defaultSprintPlugin}},\n\t\t\t},\n\t\t\tjiraApi: &JiraAPI{\n\t\t\t\tUser:        \"User\",\n\t\t\t\tIssuetype:   \"Task\",\n\t\t\t\tProjectKey:  \"Project\",\n\t\t\t\tPriority:    \"High\",\n\t\t\t\tDescription: \"Description\",\n\t\t\t\tSummary:     \"Summary\",\n\t\t\t\tSprintId:    432,\n\t\t\t\tAssignee:    []string{\"Assignee\"},\n\t\t\t},\n\t\t\tcontent: &map[string]string{\"owners\": \"owners\"},\n\t\t\twantFieldsConfig: map[string]string{\n\t\t\t\t\"Custom Issue Type\":  \"Task\",\n\t\t\t\t\"Custom Project\":     \"Project\",\n\t\t\t\t\"Custom Priority\":    \"High\",\n\t\t\t\t\"Custom Assignee\":    \"Assignee\",\n\t\t\t\t\"Custom Description\": \"Description\",\n\t\t\t\t\"Custom Summary\":     \"Summary\",\n\t\t\t\t\"Custom Sprint\":      \"432\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"happy path (custom fields)\",\n\t\t\tfields: []*jira.Field{\n\t\t\t\t{ID: \"issuetype\", Name: \"Issue Type\"},\n\t\t\t\t{ID: \"project\", Name: \"Project\"},\n\t\t\t\t{ID: \"priority\", Name: \"Priority\"},\n\t\t\t\t{ID: \"assignee\", Name: \"Assignee\"},\n\t\t\t\t{ID: \"description\", Name: \"Description\"},\n\t\t\t\t{ID: \"summary\", Name: \"Summary\"},\n\t\t\t},\n\t\t\tjiraApi: &JiraAPI{\n\t\t\t\tUser:        \"User\",\n\t\t\t\tIssuetype:   \"Task\",\n\t\t\t\tProjectKey:  \"Project\",\n\t\t\t\tPriority:    \"High\",\n\t\t\t\tDescription: \"Description\",\n\t\t\t\tSummary:     \"Summary\",\n\t\t\t\tUnknowns:    map[string]string{\"Custom field\": \"Custom field value\"},\n\t\t\t},\n\t\t\tcontent: &map[string]string{},\n\t\t\twantFieldsConfig: map[string]string{\n\t\t\t\t\"Issue Type\":   \"Task\",\n\t\t\t\t\"Project\":      \"Project\",\n\t\t\t\t\"Priority\":     \"High\",\n\t\t\t\t\"Assignee\":     \"User\",\n\t\t\t\t\"Description\":  \"Description\",\n\t\t\t\t\"Summary\":      \"Summary\",\n\t\t\t\t\"Custom field\": \"Custom field value\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path (filed.GetList() return error)\",\n\t\t\twantError: \"json: cannot unmarshal string into Go value of type []jira.Field\",\n\t\t},\n\t\t{\n\t\t\tname:      \"sad path (createIssuePriority return error)\",\n\t\t\tfields:    []*jira.Field{},\n\t\t\tjiraApi:   &JiraAPI{},\n\t\t\twantError: \"project doesn't have issue priorities\",\n\t\t},\n\t}\n\n\toldGetIssuePriority := getIssuePriority\n\tdefer func() { getIssuePriority = oldGetIssuePriority }()\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.fields, test.wantError)))\n\t\t\tdefer ts.Close()\n\n\t\t\tjiraClient, err := jira.NewClient(ts.Client(), ts.URL)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"can't create jiraClient %v\", err)\n\t\t\t}\n\n\t\t\tgetIssuePriority = func(ctx *JiraAPI, client *jira.Client) (string, error) {\n\t\t\t\tif test.wantError != \"\" {\n\t\t\t\t\treturn \"\", fmt.Errorf(test.wantError)\n\t\t\t\t} else {\n\t\t\t\t\treturn test.jiraApi.Priority, nil\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tfieldsConfig, err := createFieldsConfig(test.jiraApi, jiraClient, test.content)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, test.wantFieldsConfig, fieldsConfig)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_CreateMetaIssueType(t *testing.T) {\n\ttests := []struct {\n\t\tname              string\n\t\tmetaProject       *jira.MetaProject\n\t\tissueType         string\n\t\twantMetaIssueType *jira.MetaIssueType\n\t\twantError         string\n\t}{\n\t\t{\n\t\t\tname:              \"happy path\",\n\t\t\tmetaProject:       &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: \"Task\"}, {Name: \"Bug\"}}},\n\t\t\twantMetaIssueType: &jira.MetaIssueType{Name: \"Task\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"sad path\",\n\t\t\tmetaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: \"SubTask\"}, {Name: \"Bug\"}}},\n\t\t\twantError:   \"could not find issuetype\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tmetaIssueType, err := createMetaIssueType(test.metaProject, defaultIssueType)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, test.wantMetaIssueType, metaIssueType)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestJiraAPI_Init(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tjiraApi     *JiraAPI\n\t\tenvPassword string\n\t\twantJiraApi *JiraAPI\n\t}{\n\t\t{\n\t\t\tname:        \"happy path\",\n\t\t\tjiraApi:     &JiraAPI{BoardName: \"board0\", ProjectKey: \"project\", Password: \"password\"},\n\t\t\twantJiraApi: &JiraAPI{BoardName: \"board0\", ProjectKey: \"project\", Password: \"password\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"happy path (empty BoardName)\",\n\t\t\tjiraApi:     &JiraAPI{ProjectKey: \"project\", Password: \"password\"},\n\t\t\twantJiraApi: &JiraAPI{BoardName: \"project board\", ProjectKey: \"project\", Password: \"password\"},\n\t\t},\n\t\t{\n\t\t\tname:        \"happy path(empty password)\",\n\t\t\tjiraApi:     &JiraAPI{BoardName: \"board0\", ProjectKey: \"project\"},\n\t\t\tenvPassword: \"test_password\",\n\t\t\twantJiraApi: &JiraAPI{BoardName: \"board0\", ProjectKey: \"project\", Password: \"test_password\"},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tif test.envPassword != \"\" {\n\t\t\t\tsavedJiraPassword := os.Getenv(\"JIRA_PASSWORD\")\n\t\t\t\t_ = os.Setenv(\"JIRA_PASSWORD\", test.envPassword)\n\t\t\t\tdefer func() {\n\t\t\t\t\t_ = os.Setenv(\"JIRA_PASSWORD\", savedJiraPassword)\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\t_ = test.jiraApi.Init()\n\n\t\t\tassert.Equal(t, test.wantJiraApi, test.jiraApi)\n\t\t})\n\t}\n}\n\nfunc buildHttpHandler(successResponse interface{}, errorResponse string) func(w http.ResponseWriter, r *http.Request) {\n\tif !reflect.ValueOf(successResponse).IsNil() { // successResponse always has type therefore != nil (https://go.dev/doc/faq#nil_error)\n\t\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t\tfieldListJson, _ := json.Marshal(successResponse)\n\t\t\t_, _ = w.Write(fieldListJson)\n\t\t}\n\t} else {\n\t\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\tif errorResponse != \"\" {\n\t\t\t\t_, _ = w.Write([]byte(errorResponse))\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "actions/kubernetes.go",
    "content": "package actions\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\t\"github.com/tidwall/gjson\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/client-go/kubernetes\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n\t\"k8s.io/client-go/util/retry\"\n)\n\nconst (\n\tregoInputPrefix = \"event.input\"\n\n\tKubernetesLabelKey      = \"labels\"\n\tKubernetesAnnotationKey = \"annotations\"\n)\n\nfunc IsK8s() bool {\n\t_, ok := os.LookupEnv(\"KUBERNETES_SERVICE_HOST\")\n\treturn ok\n}\n\nfunc updateMap(old map[string]string, new map[string]string) map[string]string {\n\tnewMap := make(map[string]string)\n\tfor k, v := range old {\n\t\tnewMap[k] = v\n\t}\n\tfor k, v := range new {\n\t\tnewMap[k] = v\n\t}\n\treturn newMap\n}\n\ntype KubernetesClient struct {\n\tclientset kubernetes.Interface\n\n\tName              string\n\tKubeNamespace     string\n\tKubeConfigFile    string\n\tKubeLabelSelector string\n\tKubeActions       map[string]map[string]string\n}\n\nfunc (k KubernetesClient) GetName() string {\n\treturn k.Name\n}\n\nfunc (k *KubernetesClient) Init() error {\n\tconfig, err := clientcmd.BuildConfigFromFlags(\"\", k.KubeConfigFile)\n\tif err != nil {\n\t\tlog.Println(\"unable to initialize kubernetes config: \", err)\n\t\treturn err\n\t}\n\n\tk.clientset, err = kubernetes.NewForConfig(config)\n\tif err != nil {\n\t\tlog.Println(\"unable to initialize kubernetes client: \", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc jsonOrString(input map[string]string, filter string) string {\n\tvar ret string\n\tif json.Valid([]byte(input[\"description\"])) { // input is json\n\t\tret = gjson.Get(input[\"description\"], filter).String()\n\t} else {\n\t\tret = input[\"description\"] // input is a string\n\t}\n\treturn ret\n}\n\nfunc (k KubernetesClient) prepareInputs(input map[string]string) (string, map[string]map[string]string) {\n\tretAction := make(map[string]map[string]string)\n\tvar retLabelSelector string\n\tretLabelSelector = k.KubeLabelSelector\n\n\tif strings.Contains(k.KubeLabelSelector, regoInputPrefix) {\n\t\tretLabelSelector = jsonOrString(input, strings.TrimPrefix(k.KubeLabelSelector, regoInputPrefix+\".\"))\n\t}\n\n\tfor key, m := range k.KubeActions {\n\t\tfor id, val := range m {\n\t\t\tvar calcVal string\n\t\t\tif strings.HasPrefix(val, regoInputPrefix) {\n\t\t\t\tcalcVal = jsonOrString(input, strings.TrimPrefix(val, regoInputPrefix+\".\"))\n\t\t\t} else {\n\t\t\t\tcalcVal = val // no rego to parse\n\t\t\t}\n\t\t\tif _, ok := retAction[key][id]; !ok && len(retAction[key]) == 0 {\n\t\t\t\tretAction[key] = map[string]string{id: calcVal}\n\t\t\t} else {\n\t\t\t\tretAction[key][id] = calcVal\n\t\t\t}\n\t\t}\n\t}\n\n\treturn retLabelSelector, retAction\n}\n\nfunc (k KubernetesClient) Send(m map[string]string) error {\n\tctx := context.Background()\n\tlabelSelector, actions := k.prepareInputs(m)\n\n\t// TODO: Allow configuring of resource {pod, ds, ...}\n\tpods, _ := k.clientset.CoreV1().Pods(k.KubeNamespace).List(ctx, metav1.ListOptions{\n\t\tLabelSelector: labelSelector,\n\t})\n\tfor _, pod := range pods.Items {\n\t\tif len(actions[KubernetesLabelKey]) > 0 {\n\t\t\tretryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {\n\t\t\t\tpod, err := k.clientset.CoreV1().Pods(pod.GetNamespace()).Get(ctx, pod.Name, metav1.GetOptions{})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get updated pod for labeling: %s, err: %w\", pod.Name, err)\n\t\t\t\t}\n\n\t\t\t\tlabels := updateMap(pod.GetLabels(), actions[KubernetesLabelKey])\n\t\t\t\tpod.SetLabels(labels)\n\t\t\t\t_, err = k.clientset.CoreV1().Pods(pod.GetNamespace()).Update(ctx, pod, metav1.UpdateOptions{})\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Println(\"failed to apply labels to pod:\", pod.Name, \"err:\", err.Error(), \"retrying...\")\n\t\t\t\t\treturn err\n\t\t\t\t} else {\n\t\t\t\t\tlog.Println(\"labels applied successfully to pod:\", pod.Name)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif retryErr != nil {\n\t\t\t\tlog.Println(\"failed to apply labels to pod:\", pod.Name, \"err:\", retryErr)\n\t\t\t}\n\t\t}\n\n\t\tif len(actions[KubernetesAnnotationKey]) > 0 {\n\t\t\tretryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {\n\t\t\t\tpod, err := k.clientset.CoreV1().Pods(pod.GetNamespace()).Get(ctx, pod.Name, metav1.GetOptions{})\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"failed to get updated pod for annotating: %s, err: %w\", pod.Name, err)\n\t\t\t\t}\n\n\t\t\t\tannotations := updateMap(pod.GetAnnotations(), actions[KubernetesAnnotationKey])\n\t\t\t\tpod.SetAnnotations(annotations)\n\t\t\t\t_, err = k.clientset.CoreV1().Pods(pod.GetNamespace()).Update(ctx, pod, metav1.UpdateOptions{})\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Println(\"failed to apply annotation to pod:\", pod.Name, \"err:\", err.Error(), \"retrying...\")\n\t\t\t\t\treturn err\n\t\t\t\t} else {\n\t\t\t\t\tlog.Println(\"annotations applied successfully to pod:\", pod.Name)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tif retryErr != nil {\n\t\t\t\tlog.Println(\"failed to apply annotations to pod:\", pod.Name, \"err:\", retryErr)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (k KubernetesClient) Terminate() error {\n\tlog.Printf(\"Kubernetes output terminated\\n\")\n\treturn nil\n}\n\nfunc (k KubernetesClient) GetLayoutProvider() layout.LayoutProvider {\n\treturn nil\n}\n"
  },
  {
    "path": "actions/kubernetes_test.go",
    "content": "package actions\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/kubernetes/fake\"\n\tfake2 \"k8s.io/client-go/kubernetes/typed/core/v1/fake\"\n\tk8stesting \"k8s.io/client-go/testing\"\n)\n\nfunc TestKubernetesClientSend_Labels(t *testing.T) {\n\ttestCases := []struct {\n\t\tname               string\n\t\tinputEvent         string\n\t\treactorFunc        func(k8stesting.Action) (bool, runtime.Object, error)\n\t\tinputActions       map[string]map[string]string\n\t\tinputLabelSelector string\n\t\texpectedLabels     map[string]string\n\t}{\n\t\t{\n\t\t\tname:       \"happy path, labels are added\",\n\t\t\tinputEvent: `{\"SigMetadata\":{\"ID\":\"TRC-2\"}}`,\n\t\t\tinputActions: map[string]map[string]string{\n\t\t\t\t\"labels\": {\"foo\": \"bar\"},\n\t\t\t},\n\t\t\tinputLabelSelector: \"app=nginx\",\n\t\t\texpectedLabels: map[string]string{\n\t\t\t\t\"app\": \"nginx\",\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path, relative label selector and labels are added\",\n\t\t\tinputEvent: `{\"SigMetadata\":{\"ID\":\"TRC-2\", \"Hostname\":\"nginx\"}}`,\n\t\t\tinputActions: map[string]map[string]string{\n\t\t\t\t\"labels\": {\"foo\": \"bar\"},\n\t\t\t},\n\t\t\tinputLabelSelector: \"app=event.input.SigMetadata.Hostname\",\n\t\t\texpectedLabels: map[string]string{\n\t\t\t\t\"app\": \"nginx\",\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path, json input event, relative input labels are added\",\n\t\t\tinputEvent: `{\"SigMetadata\":{\"ID\":\"TRC-2\", \"Hostname\":\"foo.com\"}}`,\n\t\t\tinputActions: map[string]map[string]string{\n\t\t\t\t\"labels\": {\n\t\t\t\t\t\"foo\":      \"event.input.SigMetadata.ID\",\n\t\t\t\t\t\"hostname\": \"event.input.SigMetadata.Hostname\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tinputLabelSelector: \"app=nginx\",\n\t\t\texpectedLabels: map[string]string{\n\t\t\t\t\"app\":      \"nginx\",\n\t\t\t\t\"foo\":      \"TRC-2\",\n\t\t\t\t\"hostname\": \"foo.com\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path, string input event, relative input labels are added\",\n\t\t\tinputEvent: `foo bar baz`,\n\t\t\tinputActions: map[string]map[string]string{\n\t\t\t\t\"labels\": {\"foo\": \"event.input\"},\n\t\t\t},\n\t\t\tinputLabelSelector: \"app=nginx\",\n\t\t\texpectedLabels: map[string]string{\n\t\t\t\t\"app\": \"nginx\",\n\t\t\t\t\"foo\": \"foo bar baz\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:               \"sad path, unable to add label\",\n\t\t\tinputEvent:         `{\"SigMetadata\":{\"ID\":\"TRC-2\"}}`,\n\t\t\tinputLabelSelector: \"app=nginx\",\n\t\t\treactorFunc: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {\n\t\t\t\treturn true, nil, fmt.Errorf(\"failed to update label\")\n\t\t\t},\n\t\t\texpectedLabels: map[string]string{\n\t\t\t\t\"app\": \"nginx\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"sad path, no matching label selector and no labels are added\",\n\t\t\tinputEvent: `{\"SigMetadata\":{\"ID\":\"TRC-2\"}}`,\n\t\t\tinputActions: map[string]map[string]string{\n\t\t\t\t\"labels\": {\"foo\": \"bar\"},\n\t\t\t},\n\t\t\tinputLabelSelector: \"app=doesntexist\",\n\t\t\texpectedLabels: map[string]string{\n\t\t\t\t\"app\": \"nginx\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tk := KubernetesClient{\n\t\t\t\tclientset:         fake.NewSimpleClientset(),\n\t\t\t\tKubeNamespace:     \"testing\",\n\t\t\t\tKubeActions:       tc.inputActions,\n\t\t\t\tKubeLabelSelector: tc.inputLabelSelector,\n\t\t\t}\n\n\t\t\tif tc.reactorFunc != nil {\n\t\t\t\tk.clientset.CoreV1().(*fake2.FakeCoreV1).Fake.PrependReactor(\"update\", \"pods\", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {\n\t\t\t\t\treturn true, nil, fmt.Errorf(\"failed to update label\")\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tpod := &v1.Pod{\n\t\t\t\tTypeMeta: metav1.TypeMeta{\n\t\t\t\t\tKind:       \"Pod\",\n\t\t\t\t\tAPIVersion: \"v1\",\n\t\t\t\t},\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName:      \"test-pod\",\n\t\t\t\t\tNamespace: \"testing\",\n\t\t\t\t\tLabels:    map[string]string{\"app\": \"nginx\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t_, err := k.clientset.CoreV1().Pods(\"testing\").Create(context.TODO(), pod, metav1.CreateOptions{})\n\t\t\trequire.NoError(t, err, tc.name)\n\t\t\trequire.NoError(t, k.Send(map[string]string{\"description\": tc.inputEvent}), tc.name)\n\n\t\t\tpods, _ := k.clientset.CoreV1().Pods(\"testing\").Get(context.TODO(), \"test-pod\", metav1.GetOptions{})\n\t\t\tassert.Equal(t, tc.expectedLabels, pods.Labels, tc.name)\n\t\t})\n\t}\n}\n\nfunc TestKubernetesClientSend_Annotations(t *testing.T) {\n\ttestCases := []struct {\n\t\tname                string\n\t\tinputEvent          string\n\t\treactorFunc         func(k8stesting.Action) (bool, runtime.Object, error)\n\t\tinputActions        map[string]map[string]string\n\t\texpectedAnnotations map[string]string\n\t}{\n\t\t{\n\t\t\tname:       \"happy path, labels are added\",\n\t\t\tinputEvent: `{\"SigMetadata\":{\"ID\":\"TRC-2\"}}`,\n\t\t\tinputActions: map[string]map[string]string{\n\t\t\t\t\"annotations\": {\"foo\": \"bar\"},\n\t\t\t},\n\t\t\texpectedAnnotations: map[string]string{\n\t\t\t\t\"app\": \"nginx\",\n\t\t\t\t\"foo\": \"bar\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path, json input event, relative input annotations are added\",\n\t\t\tinputEvent: `{\"SigMetadata\":{\"ID\":\"TRC-2\"}}`,\n\t\t\tinputActions: map[string]map[string]string{\n\t\t\t\t\"annotations\": {\"foo\": \"event.input.SigMetadata.ID\"},\n\t\t\t},\n\t\t\texpectedAnnotations: map[string]string{\n\t\t\t\t\"app\": \"nginx\",\n\t\t\t\t\"foo\": \"TRC-2\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path, string input event, relative input annotations are added\",\n\t\t\tinputEvent: `foo bar baz`,\n\t\t\tinputActions: map[string]map[string]string{\n\t\t\t\t\"annotations\": {\"foo\": \"event.input\"},\n\t\t\t},\n\t\t\texpectedAnnotations: map[string]string{\n\t\t\t\t\"app\": \"nginx\",\n\t\t\t\t\"foo\": \"foo bar baz\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:       \"sad path, unable to add annotations\",\n\t\t\tinputEvent: `{\"SigMetadata\":{\"ID\":\"TRC-2\"}}`,\n\t\t\treactorFunc: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {\n\t\t\t\treturn true, nil, fmt.Errorf(\"failed to update label\")\n\t\t\t},\n\t\t\texpectedAnnotations: map[string]string{\n\t\t\t\t\"app\": \"nginx\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tk := KubernetesClient{\n\t\t\t\tclientset:     fake.NewSimpleClientset(),\n\t\t\t\tKubeNamespace: \"testing\",\n\t\t\t\tKubeActions:   tc.inputActions,\n\t\t\t}\n\n\t\t\tif tc.reactorFunc != nil {\n\t\t\t\tk.clientset.CoreV1().(*fake2.FakeCoreV1).Fake.PrependReactor(\"update\", \"pods\", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) {\n\t\t\t\t\treturn true, nil, fmt.Errorf(\"failed to update annotation\")\n\t\t\t\t})\n\t\t\t}\n\t\t\tpod := &v1.Pod{\n\t\t\t\tTypeMeta: metav1.TypeMeta{\n\t\t\t\t\tKind:       \"Pod\",\n\t\t\t\t\tAPIVersion: \"v1\",\n\t\t\t\t},\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName:        \"test-pod\",\n\t\t\t\t\tNamespace:   \"testing\",\n\t\t\t\t\tAnnotations: map[string]string{\"app\": \"nginx\"},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\t_, err := k.clientset.CoreV1().Pods(\"testing\").Create(context.TODO(), pod, metav1.CreateOptions{})\n\t\t\trequire.NoError(t, err, tc.name)\n\t\t\trequire.NoError(t, k.Send(map[string]string{\"description\": tc.inputEvent}), tc.name)\n\n\t\t\tpods, _ := k.clientset.CoreV1().Pods(\"testing\").Get(context.TODO(), \"test-pod\", metav1.GetOptions{})\n\t\t\tassert.Equal(t, tc.expectedAnnotations, pods.Annotations, tc.name)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "actions/message.go",
    "content": "package actions\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nconst posteeDocsUrl = \"https://aquasecurity.github.io/postee/settings/\"\n\nfunc buildShortMessage(server, urls string, provider layout.LayoutProvider) string {\n\tvar builder bytes.Buffer\n\tif len(server) > 0 && len(urls) > 0 {\n\t\tbuilder.WriteString(provider.P(\"This message is too long to display here. Please visit the link to read the content.\"))\n\t\tlinks := strings.Split(urls, \"\\n\")\n\t\tfor _, link := range links {\n\t\t\tlinkTitle, err := url.QueryUnescape(link)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Query unescape error: %s\", err)\n\t\t\t}\n\t\t\tbuilder.WriteString(provider.P(provider.A(link, linkTitle)))\n\t\t}\n\t} else if len(server) == 0 {\n\t\tbuilder.WriteString(provider.P(\"Please configure Aqua server url to get link to entire scan results.\"))\n\t\tbuilder.WriteString(provider.P(provider.A(posteeDocsUrl, \"Postee settings\")))\n\t} else {\n\t\tbuilder.WriteString(provider.P(\"Unable to create link to entire scan results. Input message doesn't contain 'registry' and 'image' fields or they are empty\"))\n\t}\n\treturn builder.String()\n}\n"
  },
  {
    "path": "actions/message_test.go",
    "content": "package actions\n\nimport (\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_buildShortMessage(t *testing.T) {\n\ttestCases := []struct {\n\t\tname        string\n\t\tprovider    layout.LayoutProvider\n\t\tinputServer string\n\t\tinputUrls   string\n\t\twant        string\n\t}{\n\t\t{\n\t\t\tname:        \"happy path with slack provider\",\n\t\t\tprovider:    new(formatting.SlackMrkdwnProvider),\n\t\t\tinputServer: \"foo.com\",\n\t\t\tinputUrls:   \"foo1.com\",\n\t\t\twant:        `{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"This message is too long to display here. Please visit the link to read the content.\"}},{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"\\u003cfoo1.com|foo1.com\\u003e\"}},`,\n\t\t},\n\t\t{\n\t\t\tname:        \"happy path with teams/html provider\",\n\t\t\tinputServer: \"foo.com\",\n\t\t\tinputUrls:   \"foo1.com\",\n\t\t\tprovider:    new(formatting.HtmlProvider),\n\t\t\twant: `<p>This message is too long to display here. Please visit the link to read the content.</p>\n<p><a href='foo1.com'>foo1.com</a></p>\n`,\n\t\t},\n\t\t{\n\t\t\tname:      \"no configured aqua server\",\n\t\t\tinputUrls: \"foo1.com\",\n\t\t\tprovider:  new(formatting.HtmlProvider),\n\t\t\twant: `<p>Please configure Aqua server url to get link to entire scan results.</p>\n<p><a href='https://aquasecurity.github.io/postee/settings/'>Postee settings</a></p>\n`,\n\t\t},\n\t\t{\n\t\t\tname:        \"no configured urls\",\n\t\t\tinputServer: \"foo.com\",\n\t\t\tprovider:    new(formatting.HtmlProvider),\n\t\t\twant: `<p>Unable to create link to entire scan results. Input message doesn't contain 'registry' and 'image' fields or they are empty</p>\n`,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot := buildShortMessage(tc.inputServer, tc.inputUrls, tc.provider)\n\t\t\tassert.Equal(t, tc.want, got, tc.name)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "actions/nexusiq.go",
    "content": "package actions\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nvar notAllowed = regexp.MustCompile(`[\\.:\\/]`)\n\nfunc sanitizedAppName(appName string) string {\n\treturn notAllowed.ReplaceAllString(appName, \"_\")\n}\n\ntype NexusIqAction struct {\n\tName           string\n\tUrl            string\n\tUser           string\n\tPassword       string\n\tOrganizationId string\n}\n\nfunc (nexus *NexusIqAction) GetName() string {\n\treturn nexus.Name\n}\n\nfunc (nexus *NexusIqAction) Init() error {\n\tlog.Printf(\"Starting Nexus IQ action %q, for sending to %q\", nexus.Name, nexus.Url)\n\treturn nil\n}\nfunc (nexus *NexusIqAction) auth() string {\n\treturn base64.StdEncoding.EncodeToString([]byte(nexus.User + \":\" + nexus.Password))\n}\n\nfunc (nexus *NexusIqAction) execute(method string, url string, payload string, headers map[string]string) (map[string]interface{}, error) {\n\tclient := http.DefaultClient\n\tclient.Timeout = time.Second * 120\n\n\tvar reader io.Reader\n\n\tif payload != \"\" {\n\t\treader = strings.NewReader(payload)\n\t}\n\n\treq, err := http.NewRequest(method, url, reader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor name, value := range headers {\n\t\treq.Header.Add(name, value)\n\n\t}\n\treq.Header.Add(\"Authorization\", \"Basic \"+nexus.auth())\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer resp.Body.Close()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode < 200 || resp.StatusCode > 299 {\n\t\tmsg := \"received incorrect response status: %d. Body: %s\"\n\t\treturn nil, fmt.Errorf(msg, resp.StatusCode, body)\n\t}\n\n\tr := make(map[string]interface{})\n\n\terr = json.Unmarshal(body, &r)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn r, nil\n\n}\n\nfunc (nexus *NexusIqAction) getAppByNameAndOrg(organizationId string, appName string) (string, error) {\n\tsanitizedAppName := sanitizedAppName(appName)\n\turl := fmt.Sprintf(\"%s/api/v2/applications/organization/%s\", nexus.Url, organizationId)\n\tr, err := nexus.execute(\"GET\", url, \"\", map[string]string{\"Content-Type\": \"application/json\"})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error fetching application: %w\", err)\n\t}\n\tapplications := r[\"applications\"].([]interface{})\n\tfor _, item := range applications {\n\t\tapp := item.(map[string]interface{})\n\t\tif app[\"publicId\"].(string) == sanitizedAppName {\n\t\t\treturn app[\"id\"].(string), nil\n\t\t}\n\t}\n\treturn \"\", nil\n}\nfunc (nexus *NexusIqAction) createApp(organizationId string, appName string) (string, error) {\n\tsanitizedAppName := sanitizedAppName(appName)\n\tpayload := map[string]string{\n\t\t\"publicId\":       sanitizedAppName,\n\t\t\"name\":           sanitizedAppName,\n\t\t\"organizationId\": organizationId,\n\t}\n\n\tb, err := json.Marshal(payload)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\turl := fmt.Sprintf(\"%s/api/v2/applications\", nexus.Url)\n\n\tr, err := nexus.execute(\"POST\", url, string(b), map[string]string{\"Content-Type\": \"application/json\"})\n\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"error creating application: %w\", err)\n\t}\n\n\treturn r[\"id\"].(string), nil\n}\n\nfunc (nexus *NexusIqAction) createOrGetApp(appName string) (string, error) {\n\tapp, err := nexus.getAppByNameAndOrg(nexus.OrganizationId, appName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif app == \"\" {\n\t\tapp, err = nexus.createApp(nexus.OrganizationId, appName)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\treturn app, nil\n}\nfunc (nexus *NexusIqAction) registerBom(appId string, bom string) error {\n\turl := fmt.Sprintf(\"%s/api/v2/scan/applications/%s/sources/cyclone\", nexus.Url, appId)\n\n\t_, err := nexus.execute(\"POST\", url, bom, map[string]string{\"Content-Type\": \"application/xml\"})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"error registering bom: %w\", err)\n\t}\n\treturn nil\n}\n\nfunc (nexus *NexusIqAction) Send(content map[string]string) error {\n\tappId, err := nexus.createOrGetApp(content[\"title\"])\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdata := content[\"description\"]\n\n\terr = nexus.registerBom(appId, data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (nexus *NexusIqAction) Terminate() error {\n\tlog.Printf(\"Nexus IQ action %q terminated.\", nexus.Name)\n\treturn nil\n}\n\nfunc (nexus *NexusIqAction) GetLayoutProvider() layout.LayoutProvider {\n\t/*TODO come up with smaller interface that doesn't include GetLayoutProvider()*/\n\treturn new(formatting.HtmlProvider)\n}\n"
  },
  {
    "path": "actions/nexusiq_test.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst createdAppId = \"cd8fd2f4f289445b8975092e7d3045ba\"\n\nfunc TestSanitizedAppName(t *testing.T) {\n\ttestCases := []struct {\n\t\tname        string\n\t\timage       string\n\t\tapplication string\n\t}{{\n\t\tname:        \"Dot\",\n\t\timage:       \"alpine-3.7\",\n\t\tapplication: \"alpine-3_7\",\n\t}, {\n\t\tname:        \"Both dot and colon\",\n\t\timage:       \"all-in-one:3.5.19223\",\n\t\tapplication: \"all-in-one_3_5_19223\",\n\t}, {\n\n\t\tname:        \"Slash\",\n\t\timage:       \"bpdockerlab/pii-data:1_0\",\n\t\tapplication: \"bpdockerlab_pii-data_1_0\",\n\t}}\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tappName := sanitizedAppName(tc.image)\n\t\t\tassert.Equal(t, tc.application, appName)\n\t\t})\n\t}\n}\n\nfunc TestNexusiq_Init(t *testing.T) {\n\tnx := NexusIqAction{}\n\trequire.NoError(t, nx.Init())\n}\n\nfunc TestNexusiq_GetName(t *testing.T) {\n\tnx := NexusIqAction{Name: \"my-nexusiq\"}\n\trequire.NoError(t, nx.Init())\n\trequire.Equal(t, \"my-nexusiq\", nx.GetName())\n}\n\nfunc TestNexusiq_Send(t *testing.T) {\n\torganizationId := \"9beee80c6fc148dfa51e8b0359ee4d4e\"\n\tapplicationsJson := fmt.Sprintf(`\n\t{\n\t\t\"applications\": [\n\t\t\t{\n\t\t\t\t\"id\": \"4bb67dcfc86344e3a483832f8c496419\",\n\t\t\t\t\"publicId\": \"alpine-3_7\",\n\t\t\t\t\"name\": \"MySecondApplication\",\n\t\t\t\t\"organizationId\": \"%s\",\n\t\t\t\t\"contactUserName\": \"NewAppContact\"\n\t\t\t}\n\t\t]\n\t}\t\n`, organizationId)\n\tcreateAppPld := fmt.Sprintf(`{\"name\":\"nginx-1_7_1\",\"organizationId\":\"%s\",\"publicId\":\"nginx-1_7_1\"}`, organizationId)\n\ttestCases := []struct {\n\t\tname               string\n\t\timage              string\n\t\tapplications       string\n\t\texpctdCreateAppPld string\n\t\texpctdAppId        string\n\t}{{\n\t\tname:         \"Existing application\",\n\t\timage:        \"alpine-3.7\",\n\t\tapplications: applicationsJson,\n\t\texpctdAppId:  \"4bb67dcfc86344e3a483832f8c496419\",\n\t}, {\n\t\tname:               \"New application\",\n\t\timage:              \"nginx-1.7.1\",\n\t\tapplications:       applicationsJson,\n\t\texpctdCreateAppPld: createAppPld,\n\t\texpctdAppId:        createdAppId,\n\t},\n\t}\n\n\tb, err := ioutil.ReadFile(\"testdata/nexus-iq-sbom.xml\")\n\tif err != nil {\n\t\tt.Fatal(\"unable to read test data %w\", err)\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tinput := map[string]string{\n\t\t\t\t\"title\":       tc.image,\n\t\t\t\t\"description\": string(b),\n\t\t\t}\n\t\t\tts := configureHttp(t, tc.applications, tc.expctdCreateAppPld, tc.expctdAppId)\n\t\t\tnx := NexusIqAction{Name: \"my-nexusiq\", Url: ts.URL, User: \"admin\", Password: \"admin\", OrganizationId: \"9beee80c6fc148dfa51e8b0359ee4d4e\"}\n\t\t\trequire.NoError(t, nx.Send(input))\n\t\t\tdefer ts.Close()\n\t\t})\n\t}\n\n}\n\nfunc configureHttp(t *testing.T, applicationsJson, expctdCreateAppPld, expctdAppId string) *httptest.Server {\n\trouter := mux.NewRouter()\n\n\t//get applications\n\trouter.HandleFunc(\"/api/v2/applications/organization/{organization:[a-z0-9]+}\", func(w http.ResponseWriter, r *http.Request) {\n\t\tassert.Equal(t, \"GET\", r.Method)\n\t\t_, _ = w.Write([]byte(applicationsJson))\n\t})\n\n\t//create application\n\trouter.HandleFunc(\"/api/v2/applications\", func(w http.ResponseWriter, r *http.Request) {\n\t\tbody, err := io.ReadAll(r.Body)\n\n\t\trequire.NoError(t, err)\n\n\t\tassert.Equal(t, \"POST\", r.Method)\n\t\tassert.Equal(t, expctdCreateAppPld, string(body))\n\n\t\t_, _ = w.Write([]byte(fmt.Sprintf(`{\"id\":\"%s\"}`, createdAppId)))\n\t})\n\n\t//register bom\n\trouter.HandleFunc(\"/api/v2/scan/applications/{app:[a-z0-9]+}/sources/cyclone\", func(w http.ResponseWriter, r *http.Request) {\n\t\tvars := mux.Vars(r)\n\t\tassert.Equal(t, \"POST\", r.Method)\n\t\tassert.Equal(t, expctdAppId, vars[\"app\"])\n\n\t\t_, _ = w.Write([]byte(\"{}\"))\n\t})\n\n\treturn httptest.NewServer(router)\n}\n"
  },
  {
    "path": "actions/opsgenie.go",
    "content": "package actions\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/opsgenie/opsgenie-go-sdk-v2/alert\"\n\t\"github.com/opsgenie/opsgenie-go-sdk-v2/client\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nconst defaultPriority = alert.P3\n\ntype OpsGenieAction struct {\n\tName           string\n\tUser           string\n\tAPIKey         string\n\tResponders     []string\n\tVisibleTo      []string\n\tTags           []string\n\tAlias          string\n\tEntity         string\n\tPrioritySource string\n\tpriority       alert.Priority\n\n\tclient *alert.Client\n}\n\nfunc (ops *OpsGenieAction) GetName() string {\n\treturn ops.Name\n}\n\nfunc (ops *OpsGenieAction) Init() (err error) {\n\tops.client, err = alert.NewClient(&client.Config{\n\t\tApiKey: ops.APIKey,\n\t})\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif ops.PrioritySource != \"\" {\n\t\tops.priority = alert.Priority(ops.PrioritySource)\n\t} else {\n\t\tops.priority = defaultPriority\n\t}\n\n\tlog.Printf(\"Starting OpsGenie action %q....\", ops.Name)\n\treturn nil\n}\n\nfunc getUserResponders(users []string) []alert.Responder {\n\tif len(users) == 0 {\n\t\treturn nil\n\t}\n\tresponders := []alert.Responder{}\n\tfor _, user := range users {\n\t\tresponder := alert.Responder{Type: alert.UserResponder, Username: user}\n\t\tresponders = append(responders, responder)\n\t}\n\treturn responders\n}\n\nfunc (ops *OpsGenieAction) convertResultToOpsGenie(title string, content map[string]interface{}) *alert.CreateAlertRequest {\n\tdescription := \"\"\n\tif content[\"description\"] != nil {\n\t\tdescription = fmt.Sprint(content[\"description\"])\n\t}\n\n\talias := ops.Alias\n\tif content[\"alias\"] != nil {\n\t\talias = fmt.Sprint(content[\"alias\"])\n\t}\n\n\tentity := ops.Entity\n\tif content[\"entity\"] != nil {\n\t\tentity = fmt.Sprint(content[\"entity\"])\n\t}\n\n\tpriority := ops.priority\n\tif content[\"priority\"] != nil {\n\t\tpriority = alert.Priority(fmt.Sprint(content[\"priority\"]))\n\t}\n\ttags := ops.Tags\n\tif content[\"tags\"] != nil {\n\t\tswitch content[\"tags\"].(type) {\n\t\tcase []string:\n\t\t\ttags = append(tags, content[\"tags\"].([]string)...)\n\t\tcase string:\n\t\t\ttags = append(tags, strings.Split(content[\"tags\"].(string), \",\")...)\n\t\t}\n\t}\n\n\treturn &alert.CreateAlertRequest{\n\t\tMessage:     title,\n\t\tDescription: description,\n\t\tAlias:       alias,\n\t\tEntity:      entity,\n\t\tPriority:    priority,\n\t\tTags:        tags,\n\t\tResponders:  getUserResponders(ops.Responders),\n\t\tVisibleTo:   getUserResponders(ops.VisibleTo),\n\t}\n}\n\nfunc (ops *OpsGenieAction) Send(input map[string]string) error {\n\tdata := map[string]interface{}{}\n\tif err := json.Unmarshal([]byte(input[\"description\"]), &data); err != nil {\n\t\treturn err\n\t}\n\tr := ops.convertResultToOpsGenie(input[\"title\"], data)\n\tr.User = ops.User\n\n\talertResult, err := ops.client.Create(context.Background(), r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlog.Printf(\"Sending to %q was successful: %s\", ops.Name, alertResult.Result)\n\treturn nil\n}\n\nfunc (*OpsGenieAction) Terminate() error {\n\tlog.Println(\"Terminating OpsGenie Action\")\n\treturn nil\n}\n\nfunc (ops *OpsGenieAction) GetLayoutProvider() layout.LayoutProvider {\n\t/*TODO come up with smaller interface that doesn't include GetLayoutProvider()*/\n\treturn new(formatting.SlackMrkdwnProvider)\n}\n"
  },
  {
    "path": "actions/opsgenie_test.go",
    "content": "package actions\n\nimport (\n\t\"testing\"\n\n\t\"github.com/opsgenie/opsgenie-go-sdk-v2/alert\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestGetUserResponders(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tusers      []string\n\t\tresponders []alert.Responder\n\t}{\n\t\t{\n\t\t\tname:  \"good way\",\n\t\t\tusers: []string{\"user1\", \"user2\"},\n\t\t\tresponders: []alert.Responder{\n\t\t\t\t{Type: alert.UserResponder, Username: \"user1\"},\n\t\t\t\t{Type: alert.UserResponder, Username: \"user2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"without users\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tgot := getUserResponders(test.users)\n\t\t\tassert.Equal(t, test.responders, got)\n\t\t})\n\t}\n}\n\nfunc TestConvertResultToOpsGenie(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\ttitle  string\n\t\tdata   map[string]interface{}\n\t\tresult *alert.CreateAlertRequest\n\t}{\n\t\t{\n\t\t\tname:  \"good way\",\n\t\t\ttitle: \"all-in-one:3.5.19223\",\n\t\t\tdata: map[string]interface{}{\n\t\t\t\t\"description\": \"all-in-one:3.5.19223 vulnerability scan report\",\n\t\t\t\t\"alias\":       \"all-in-one:3.5.19223\",\n\t\t\t\t\"entity\":      \"entity\",\n\t\t\t\t\"priority\":    \"P4\",\n\t\t\t\t\"tags\":        []string{\"tag1\", \"tag2\"},\n\t\t\t},\n\t\t\tresult: &alert.CreateAlertRequest{\n\t\t\t\tMessage:     \"all-in-one:3.5.19223\",\n\t\t\t\tPriority:    alert.P4,\n\t\t\t\tDescription: \"all-in-one:3.5.19223 vulnerability scan report\",\n\t\t\t\tAlias:       \"all-in-one:3.5.19223\",\n\t\t\t\tEntity:      \"entity\",\n\t\t\t\tTags:        []string{\"tag1\", \"tag2\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"only title\",\n\t\t\ttitle: \"all-in-one:3.5.19223\",\n\t\t\tdata:  map[string]interface{}{},\n\t\t\tresult: &alert.CreateAlertRequest{\n\t\t\t\tMessage:  \"all-in-one:3.5.19223\",\n\t\t\t\tPriority: alert.P3,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:  \"good way with tags as string\",\n\t\t\ttitle: \"all-in-one:3.5.19223\",\n\t\t\tdata: map[string]interface{}{\n\t\t\t\t\"description\": \"all-in-one:3.5.19223 vulnerability scan report\",\n\t\t\t\t\"alias\":       \"all-in-one:3.5.19223\",\n\t\t\t\t\"entity\":      \"entity\",\n\t\t\t\t\"priority\":    \"P4\",\n\t\t\t\t\"tags\":        \"tag1,tag2\",\n\t\t\t},\n\t\t\tresult: &alert.CreateAlertRequest{\n\t\t\t\tMessage:     \"all-in-one:3.5.19223\",\n\t\t\t\tPriority:    alert.P4,\n\t\t\t\tDescription: \"all-in-one:3.5.19223 vulnerability scan report\",\n\t\t\t\tAlias:       \"all-in-one:3.5.19223\",\n\t\t\t\tEntity:      \"entity\",\n\t\t\t\tTags:        []string{\"tag1\", \"tag2\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tops := &OpsGenieAction{\n\t\t\t\tAPIKey: \"anyAPIkey\",\n\t\t\t}\n\t\t\terr := ops.Init()\n\t\t\tassert.NoError(t, err)\n\t\t\tr := ops.convertResultToOpsGenie(test.title, test.data)\n\t\t\tassert.Equal(t, test.result, r)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "actions/pagerduty.go",
    "content": "package actions\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/PagerDuty/go-pagerduty\"\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\ntype Clock interface {\n\tNow() time.Time\n}\n\ntype realClock struct{}\n\nfunc (rc *realClock) Now() time.Time {\n\treturn time.Now()\n}\n\ntype PagerdutyClient struct {\n\tclient *pagerduty.Client\n\tclock  Clock\n\n\tName       string\n\tAuthToken  string\n\tRoutingKey string\n}\n\nfunc (p *PagerdutyClient) GetName() string {\n\treturn p.Name\n}\n\nfunc (p *PagerdutyClient) Init() error {\n\tif len(p.AuthToken) <= 0 {\n\t\treturn fmt.Errorf(\"pagerduty auth token is required to send events\")\n\t}\n\tif len(p.RoutingKey) <= 0 {\n\t\treturn fmt.Errorf(\"pagerduty routing key is required to send events\")\n\t}\n\n\tp.client = pagerduty.NewClient(p.AuthToken)\n\tp.clock = &realClock{}\n\treturn nil\n}\n\nfunc (p *PagerdutyClient) Send(m map[string]string) error {\n\tctx := context.Background()\n\tresp, err := p.client.ManageEventWithContext(ctx, &pagerduty.V2Event{\n\t\tRoutingKey: p.RoutingKey,\n\t\tAction:     \"trigger\",\n\t\tPayload: &pagerduty.V2Payload{\n\t\t\tSummary:   m[\"title\"], // required\n\t\t\tSource:    \"postee\",\n\t\t\tSeverity:  \"critical\",\n\t\t\tTimestamp: p.clock.Now().Format(time.RFC3339),\n\t\t\tDetails:   m[\"description\"], // required\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to send event to pagerduty: %w\", err)\n\t}\n\n\tlog.Printf(\"successfully sent event to pagerduty, response msg: %s, status: %s\", resp.Message, resp.Status)\n\treturn nil\n}\n\nfunc (p *PagerdutyClient) Terminate() error {\n\treturn nil\n}\n\nfunc (p *PagerdutyClient) GetLayoutProvider() layout.LayoutProvider {\n\t/*TODO come up with smaller interface that doesn't include GetLayoutProvider()*/\n\treturn new(formatting.HtmlProvider)\n}\n"
  },
  {
    "path": "actions/pagerduty_test.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/PagerDuty/go-pagerduty\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype fakeClock struct{}\n\nfunc (fc *fakeClock) Now() time.Time {\n\tt, _ := time.Parse(time.RFC3339, \"2022-09-22T22:07:55-07:00\")\n\treturn t\n}\n\nfunc TestPagerdutyClient_Init(t *testing.T) {\n\tt.Run(\"happy path\", func(t *testing.T) {\n\t\trequire.NoError(t, (&PagerdutyClient{\n\t\t\tName:       \"my-pagerduty\",\n\t\t\tAuthToken:  \"123456\",\n\t\t\tRoutingKey: \"foobarbaz\",\n\t\t}).Init())\n\t})\n\n\tt.Run(\"sad path, no auth token\", func(t *testing.T) {\n\t\tassert.Equal(t, \"pagerduty auth token is required to send events\", (&PagerdutyClient{\n\t\t\tName:       \"my-pagerduty\",\n\t\t\tRoutingKey: \"foobarbaz\",\n\t\t}).Init().Error())\n\t})\n\n\tt.Run(\"sad path, no routing key\", func(t *testing.T) {\n\t\tassert.Equal(t, \"pagerduty routing key is required to send events\", (&PagerdutyClient{\n\t\t\tName:      \"my-pagerduty\",\n\t\t\tAuthToken: \"123456\",\n\t\t}).Init().Error())\n\t})\n}\n\nfunc TestPagerdutyClient_Send(t *testing.T) {\n\ttestCases := []struct {\n\t\tname            string\n\t\thandlerFunc     http.HandlerFunc\n\t\texpectedError   string\n\t\tpagerdutyClient PagerdutyClient\n\t\tinputEvent      map[string]string\n\t}{\n\t\t{\n\t\t\tname: \"happy path\",\n\t\t\thandlerFunc: func(writer http.ResponseWriter, request *http.Request) {\n\t\t\t\tb, _ := io.ReadAll(request.Body)\n\t\t\t\tassert.JSONEq(t, `{\"routing_key\":\"123456\",\"event_action\":\"trigger\",\"payload\":{\"summary\":\"my fancy title\",\"source\":\"postee\",\"severity\":\"critical\",\"timestamp\":\"2022-09-22T22:07:55-07:00\",\"custom_details\":\"foo bar baz details\"}}`, string(b))\n\t\t\t\t_, _ = fmt.Fprint(writer, `{\"status\": \"ok\", \"dedup_key\": \"yes\", \"message\": \"ok\"}`)\n\t\t\t},\n\t\t\tpagerdutyClient: PagerdutyClient{\n\t\t\t\tName:       \"my-pagerduty\",\n\t\t\t\tAuthToken:  \"foo-bar-baz\",\n\t\t\t\tRoutingKey: \"123456\",\n\t\t\t},\n\t\t\tinputEvent: map[string]string{\n\t\t\t\t\"description\": \"foo bar baz details\",\n\t\t\t\t\"title\":       \"my fancy title\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"sad path, pagerduty api returns an error\",\n\t\t\thandlerFunc: func(writer http.ResponseWriter, request *http.Request) {\n\t\t\t\twriter.WriteHeader(http.StatusInternalServerError)\n\t\t\t},\n\t\t\tpagerdutyClient: PagerdutyClient{\n\t\t\t\tName:       \"my-pagerduty\",\n\t\t\t\tAuthToken:  \"foo-bar-baz\",\n\t\t\t\tRoutingKey: \"123456\",\n\t\t\t},\n\t\t\tinputEvent: map[string]string{\n\t\t\t\t\"description\": \"foo bar baz details\",\n\t\t\t\t\"title\":       \"my fancy title\",\n\t\t\t},\n\t\t\texpectedError: \"failed to send event to pagerduty: HTTP response with status code 500 does not contain Content-Type: application/json\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tts := httptest.NewServer(tc.handlerFunc)\n\t\t\tdefer ts.Close()\n\n\t\t\ttc.pagerdutyClient.client = pagerduty.NewClient(tc.pagerdutyClient.AuthToken, pagerduty.WithV2EventsAPIEndpoint(ts.URL))\n\t\t\ttc.pagerdutyClient.clock = &fakeClock{}\n\n\t\t\terr := tc.pagerdutyClient.Send(tc.inputEvent)\n\t\t\tswitch {\n\t\t\tcase tc.expectedError != \"\":\n\t\t\t\tassert.Equal(t, tc.expectedError, err.Error(), tc.name)\n\t\t\tdefault:\n\t\t\t\tassert.NoError(t, err, tc.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "actions/plugin.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nconst (\n\tApplicationScopeOwner = \"<%application_scope_owner%>\"\n)\n\ntype Action interface {\n\tGetName() string\n\tInit() error\n\tSend(map[string]string) error\n\tTerminate() error\n\tGetLayoutProvider() layout.LayoutProvider\n}\n\nfunc getHandledRecipients(recipients []string, content *map[string]string, outputName string) []string {\n\tvar result []string\n\tfor _, r := range recipients {\n\t\tif r == ApplicationScopeOwner {\n\t\t\towners, err := getAppScopeOwners(content)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"get application scope owners error for %q: %v\", outputName, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tresult = append(result, owners...)\n\t\t} else {\n\t\t\tresult = append(result, r)\n\t\t}\n\t}\n\treturn result\n}\n\nfunc getAppScopeOwners(content *map[string]string) ([]string, error) {\n\townersIn, ok := (*content)[\"owners\"]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"recipients field contains %q, but received a webhook without this data\",\n\t\t\tApplicationScopeOwner)\n\t}\n\treturn strings.Split(ownersIn, \";\"), nil\n}\n"
  },
  {
    "path": "actions/servicenow.go",
    "content": "package actions\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\tservicenow \"github.com/aquasecurity/postee/v2/servicenow\"\n)\n\ntype ServiceNowAction struct {\n\tName           string\n\tUser           string\n\tPassword       string\n\tInstance       string\n\tTable          string\n\tlayoutProvider layout.LayoutProvider\n}\n\nfunc (sn *ServiceNowAction) GetName() string {\n\treturn sn.Name\n}\n\nfunc (sn *ServiceNowAction) Init() error {\n\tlog.Printf(\"Starting ServiceNow action %q....\", sn.Name)\n\tlog.Printf(\"Your ServiceNow Table is %q on '%s.%s'\", sn.Table, sn.Instance, servicenow.BaseServer)\n\tsn.layoutProvider = new(formatting.HtmlProvider)\n\treturn nil\n}\n\nfunc (sn *ServiceNowAction) Send(content map[string]string) error {\n\tlog.Printf(\"Sending via ServiceNow %q\", sn.Name)\n\t// parse date\n\tdate := \"\"\n\tif i, err := strconv.ParseInt(content[\"date\"], 10, 64); err == nil {\n\t\tdate = time.Unix(i, 0).Format(\"2006-01-02 15:04:05\")\n\t}\n\n\t// parse severity\n\tseverity := 3 // default ServiceNow value\n\tif s, err := strconv.Atoi(content[\"severity\"]); err == nil {\n\t\tseverity = s\n\t}\n\n\td := &servicenow.ServiceNowData{\n\t\tOpened:           date,\n\t\tShortDescription: content[\"title\"],\n\t\tCaller:           sn.User,\n\t\tCategory:         content[\"category\"],\n\t\tImpact:           severity,\n\t\tUrgency:          severity,\n\t\tSubcategory:      content[\"subcategory\"],\n\t\tAssignedTo:       content[\"assignedTo\"],\n\t\tAssignmentGroup:  content[\"assignedGroup\"],\n\t\tWorkNotes:        \"[code]\" + content[\"description\"] + \"[/code]\",\n\t\tDescription:      content[\"summary\"],\n\t}\n\tbody, err := json.Marshal(d)\n\tif err != nil {\n\t\tlog.Println(\"ServiceNow Error:\", err)\n\t\treturn err\n\t}\n\terr = servicenow.InsertRecordToTable(sn.User, sn.Password, sn.Instance, sn.Table, body)\n\tif err != nil {\n\t\tlog.Println(\"ServiceNow Error:\", err)\n\t\treturn err\n\t}\n\tlog.Printf(\"Sending via ServiceNow %q was successful!\", sn.Name)\n\treturn nil\n}\n\nfunc (sn *ServiceNowAction) Terminate() error {\n\tlog.Printf(\"ServiceNow action %q terminated\", sn.Name)\n\treturn nil\n}\n\nfunc (sn *ServiceNowAction) GetLayoutProvider() layout.LayoutProvider {\n\treturn sn.layoutProvider\n}\n"
  },
  {
    "path": "actions/slack.go",
    "content": "package actions\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\n\tslackAPI \"github.com/aquasecurity/postee/v2/slack\"\n)\n\nconst (\n\tslackBlockLimit = 49\n)\n\ntype SlackAction struct {\n\tName        string\n\tAquaServer  string\n\tUrl         string\n\tslackLayout layout.LayoutProvider\n}\n\nfunc (slack *SlackAction) GetName() string {\n\treturn slack.Name\n}\n\nfunc (slack *SlackAction) Init() error {\n\tslack.slackLayout = new(formatting.SlackMrkdwnProvider)\n\tlog.Printf(\"Starting Slack action %q....\", slack.Name)\n\treturn nil\n}\n\nfunc clearSlackText(text string) string {\n\ts := strings.ReplaceAll(text, \"&\", \"&amp;\")\n\ts = strings.ReplaceAll(s, \"<\", \"&lt;\")\n\ts = strings.ReplaceAll(s, \">\", \"&gt;\")\n\treturn s\n}\n\nfunc buildSlackBlock(title string, data []byte) []byte {\n\tvar content bytes.Buffer\n\tcontent.WriteByte('{')\n\tcontent.WriteString(\"\\\"blocks\\\":\")\n\tcontent.WriteByte('[')\n\tcontent.WriteString(title)\n\tcontent.Write(data)\n\tcontent.WriteByte(']')\n\tcontent.WriteByte('}')\n\treturn content.Bytes()\n}\n\nfunc (slack *SlackAction) Send(input map[string]string) error {\n\tlog.Printf(\"Sending via Slack %q\", slack.Name)\n\ttitle := clearSlackText(slack.slackLayout.TitleH2(input[\"title\"]))\n\tvar body string\n\tif strings.HasSuffix(input[\"description\"], \",\") {\n\t\tbody = strings.TrimSuffix(input[\"description\"], \",\")\n\t} else {\n\t\tbody = input[\"description\"]\n\t}\n\tbody = clearSlackText(body)\n\tif !strings.HasPrefix(body, \"[\") {\n\t\tbody = \"[\" + body + \"]\"\n\t}\n\n\tif !json.Valid([]byte(body)) {\n\t\treturn errors.New(\"wrong template selected, choose a correct template\")\n\t}\n\n\trawBlock := make([]data.SlackBlock, 0)\n\terr := json.Unmarshal([]byte(body), &rawBlock)\n\tif err != nil {\n\t\tlog.Printf(\"Unmarshal slack sending error: %v\", err)\n\t\treturn err\n\t}\n\n\tlength := len(rawBlock)\n\n\tif length >= slackBlockLimit {\n\t\tmessage := buildShortMessage(slack.AquaServer, input[\"url\"], slack.slackLayout)\n\t\tif err := slackAPI.SendToUrl(slack.Url, buildSlackBlock(title, []byte(message))); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tlog.Printf(\"Sending via Slack %q was successful!\", slack.Name)\n\t} else {\n\t\tfor n := 0; n < length; {\n\t\t\td := length - n\n\t\t\tif d >= 49 {\n\t\t\t\td = 49\n\t\t\t}\n\t\t\tcutData, _ := json.Marshal(rawBlock[n : n+d])\n\t\t\tcutData = cutData[1 : len(cutData)-1]\n\t\t\tif err := slackAPI.SendToUrl(slack.Url, buildSlackBlock(title, cutData)); err != nil {\n\t\t\t\tlog.Printf(\"Sending to %q was finished with error: %v\", slack.Name, err)\n\t\t\t\treturn err\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"Sending [%d/%d part] to %q was successful!\",\n\t\t\t\t\tint(n/49)+1, int(length/49)+1,\n\t\t\t\t\tslack.Name)\n\t\t\t}\n\t\t\tn += d\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (slack *SlackAction) Terminate() error {\n\tlog.Printf(\"Slack output %q terminated\", slack.Name)\n\treturn nil\n}\n\nfunc (slack *SlackAction) GetLayoutProvider() layout.LayoutProvider {\n\treturn slack.slackLayout\n}\n"
  },
  {
    "path": "actions/splunk.go",
    "content": "package actions\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nconst defaultSizeLimit = 10000\n\ntype SplunkAction struct {\n\tName         string\n\tUrl          string\n\tToken        string\n\tEventLimit   int\n\tTlsVerify    bool\n\tsplunkLayout layout.LayoutProvider\n}\n\nfunc (splunk *SplunkAction) GetName() string {\n\treturn splunk.Name\n}\n\nfunc (splunk *SplunkAction) Init() error {\n\tsplunk.splunkLayout = new(formatting.HtmlProvider)\n\tlog.Printf(\"Starting Splunk action %q....\", splunk.Name)\n\treturn nil\n}\n\nfunc (splunk *SplunkAction) Send(d map[string]string) error {\n\tlog.Printf(\"Sending a message to %q\", splunk.Name)\n\n\tif splunk.EventLimit == 0 {\n\t\tsplunk.EventLimit = defaultSizeLimit\n\t}\n\tif splunk.EventLimit < defaultSizeLimit {\n\t\tlog.Printf(\"[WARNING] %q has a short limit %d (default %d)\",\n\t\t\tsplunk.Name, splunk.EventLimit, defaultSizeLimit)\n\t}\n\n\tif !strings.HasSuffix(splunk.Url, \"/\") {\n\t\tsplunk.Url += \"/\"\n\t}\n\n\tscanInfo := new(data.ScanImageInfo)\n\tbody := []byte(d[\"description\"])\n\tif !json.Valid([]byte(body)) {\n\t\treturn errors.New(\"wrong template selected, choose a correct template\")\n\t}\n\n\terr := json.Unmarshal(body, scanInfo)\n\tif err != nil {\n\t\tlog.Printf(\"sending to %q error: %v\", splunk.Name, err)\n\t\treturn err\n\t}\n\n\teventFormat := \"{\\\"sourcetype\\\": \\\"_json\\\", \\\"event\\\": \"\n\tconstLimit := len(eventFormat) - 1\n\n\tvar fields []byte\n\n\tfor {\n\t\tfields, err = json.Marshal(scanInfo)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"sending to %q error: %v\", splunk.Name, err)\n\t\t\treturn err\n\t\t}\n\t\tif len(fields) < splunk.EventLimit-constLimit {\n\t\t\tbreak\n\t\t}\n\t\tswitch {\n\t\tcase len(scanInfo.Resources) > 0:\n\t\t\tscanInfo.Resources = nil\n\t\t\tcontinue\n\t\tcase len(scanInfo.Malwares) > 0:\n\t\t\tscanInfo.Malwares = nil\n\t\t\tcontinue\n\t\tcase len(scanInfo.SensitiveData) > 0:\n\t\t\tscanInfo.SensitiveData = nil\n\t\t\tcontinue\n\t\tdefault:\n\t\t\tmsg := fmt.Sprintf(\"Scan result for %q is large for %q , its size if %d (limit %d)\",\n\t\t\t\tscanInfo.Image, splunk.Name, len(fields), splunk.EventLimit)\n\t\t\tlog.Print(msg)\n\t\t\treturn errors.New(msg)\n\t\t}\n\t}\n\n\tvar buff bytes.Buffer\n\tbuff.WriteString(eventFormat)\n\tbuff.Write(fields)\n\tbuff.WriteByte('}')\n\n\treq, err := http.NewRequest(\"POST\", splunk.Url+\"services/collector\", &buff)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Add(\"Authorization\", \"Splunk \"+splunk.Token)\n\n\tclient := http.Client{\n\t\t// default transport with tls config added\n\t\tTransport: &http.Transport{\n\t\t\tProxy: http.ProxyFromEnvironment,\n\t\t\tDialContext: (&net.Dialer{\n\t\t\t\tTimeout:   30 * time.Second,\n\t\t\t\tKeepAlive: 30 * time.Second,\n\t\t\t}).DialContext,\n\t\t\tForceAttemptHTTP2:     true,\n\t\t\tMaxIdleConns:          100,\n\t\t\tIdleConnTimeout:       90 * time.Second,\n\t\t\tTLSHandshakeTimeout:   10 * time.Second,\n\t\t\tExpectContinueTimeout: 1 * time.Second,\n\t\t\tTLSClientConfig:       &tls.Config{InsecureSkipVerify: splunk.TlsVerify},\n\t\t},\n\t}\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\tdefer resp.Body.Close()\n\t\tb, _ := ioutil.ReadAll(resp.Body)\n\t\tlog.Printf(\"Splunk sending error: failed response status %q. Body: %q\", resp.Status, string(b))\n\t\treturn errors.New(\"failed response status for Splunk sending\")\n\t}\n\tlog.Printf(\"Sending a message to %q was successful!\", splunk.Name)\n\treturn nil\n}\n\nfunc (splunk *SplunkAction) Terminate() error {\n\tlog.Printf(\"Splunk action %q terminated\", splunk.Name)\n\treturn nil\n}\n\nfunc (splunk *SplunkAction) GetLayoutProvider() layout.LayoutProvider {\n\treturn splunk.splunkLayout\n}\n"
  },
  {
    "path": "actions/stdout.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\ntype StdoutAction struct {\n\tName string\n}\n\nfunc (stdout StdoutAction) GetName() string { return stdout.Name }\nfunc (stdout StdoutAction) Init() error {\n\treturn nil\n}\nfunc (stdout StdoutAction) Send(data map[string]string) error {\n\t_, err := fmt.Fprintf(os.Stdout, \"%s\", data[\"description\"])\n\treturn err\n}\nfunc (stdout StdoutAction) Terminate() error {\n\treturn nil\n}\nfunc (stdout StdoutAction) GetLayoutProvider() layout.LayoutProvider {\n\treturn &formatting.HtmlProvider{}\n}\n"
  },
  {
    "path": "actions/teams.go",
    "content": "package actions\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\t\"github.com/aquasecurity/postee/v2/utils\"\n\n\tmsteams \"github.com/aquasecurity/postee/v2/teams\"\n)\n\nconst (\n\tteamsSizeLimit = 18000 // 28 KB is an approximate limit for MS Teams\n)\n\ntype TeamsAction struct {\n\tName        string\n\tAquaServer  string\n\tteamsLayout layout.LayoutProvider\n\tWebhook     string\n}\n\nfunc (teams *TeamsAction) GetName() string {\n\treturn teams.Name\n}\n\nfunc (teams *TeamsAction) Init() error {\n\tlog.Printf(\"Starting MS Teams action %q....\", teams.Name)\n\tteams.teamsLayout = new(formatting.HtmlProvider)\n\treturn nil\n}\n\nfunc (teams *TeamsAction) Send(input map[string]string) error {\n\tlog.Printf(\"Sending to MS Teams via %q...\", teams.Name)\n\tutils.Debug(\"Title for %q: %q\\n\", teams.Name, input[\"title\"])\n\tutils.Debug(\"Url(s) for %q: %q\\n\", teams.Name, input[\"url\"])\n\tutils.Debug(\"Webhook for %q: %q\\n\", teams.Name, teams.Webhook)\n\tutils.Debug(\"Length of Description for %q: %d/%d\\n\",\n\t\tteams.Name, len(input[\"description\"]), teamsSizeLimit)\n\n\tvar body string\n\tif len(input[\"description\"]) > teamsSizeLimit {\n\t\tutils.Debug(\"MS Team action will send SHORT message\\n\")\n\t\tbody = buildShortMessage(teams.AquaServer, input[\"url\"], teams.teamsLayout)\n\t} else {\n\t\tutils.Debug(\"MS Team action will send LONG message\\n\")\n\t\tbody = input[\"description\"]\n\t}\n\tutils.Debug(\"Message is: %q\\n\", body)\n\n\tescaped, err := escapeJSON(body)\n\tif err != nil {\n\t\tlog.Printf(\"Error while escaping payload: %v\", err)\n\t\treturn err\n\t}\n\n\terr = msteams.CreateMessageByWebhook(teams.Webhook, teams.teamsLayout.TitleH2(input[\"title\"])+escaped)\n\n\tif err != nil {\n\t\tlog.Printf(\"TeamsAction Send Error: %v\", err)\n\t\treturn err\n\t}\n\n\tlog.Printf(\"Sending to MS Teams via %q was successful!\", teams.Name)\n\treturn nil\n}\n\nfunc (teams *TeamsAction) Terminate() error {\n\tlog.Printf(\"MS Teams action %q terminated\", teams.Name)\n\treturn nil\n}\n\nfunc (teams *TeamsAction) GetLayoutProvider() layout.LayoutProvider {\n\treturn teams.teamsLayout\n}\n\nfunc escapeJSON(s string) (string, error) {\n\tb, err := json.Marshal(s)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\t// Trim the beginning and trailing \" character\n\treturn string(b[1 : len(b)-1]), nil\n}\n"
  },
  {
    "path": "actions/testdata/nexus-iq-sbom.xml",
    "content": "<?xml version=\"1.0\"?>\n<bom serialNumber=\"urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79\" version=\"1\"\n    xmlns=\"http://cyclonedx.org/schema/bom/1.1\"\n    xmlns:v=\"http://cyclonedx.org/schema/ext/vulnerability/1.0\">\n    <components>\n        <component type=\"application\">\n            <name>musl</name>\n            <version>1.1.18-r3</version>\n            <licenses>\n                <license>\n                    <id>MIT</id>\n                </license>\n            </licenses>\n            <v:vulnerabilities>\n                <v:vulnerability>\n                    <v:id>CVE-2019-14697</v:id>\n                    <v:source name=\"NVD\">\n                        <v:url>https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-14697</v:url>\n                    </v:source>\n                    <v:ratings>\n                        <v:rating>\n                            <v:score>\n                                <v:base>9.8</v:base>\n                                <v:impact>9.8</v:impact>\n                                <v:exploitability>9.8</v:exploitability>\n                            </v:score>\n                            <v:severity>critical</v:severity>\n                            <v:method>CVSS V3</v:method>\n                            <v:vector>CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H</v:vector>\n                        </v:rating>\n                    </v:ratings>\n                    <v:recommendations>\n                        <v:recommendation>Upgrade package musl to version 1.1.18-r4 or above.</v:recommendation>\n                    </v:recommendations>\n                </v:vulnerability>\n            </v:vulnerabilities>\n        </components>\n    </bom>"
  },
  {
    "path": "actions/webhook.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\ntype WebhookAction struct {\n\tName    string\n\tUrl     string\n\tTimeout string\n}\n\nfunc (webhook *WebhookAction) GetName() string {\n\treturn webhook.Name\n}\n\nfunc (webhook *WebhookAction) Init() error {\n\tlog.Printf(\"Starting Webhook action %q, for sending to %q\",\n\t\twebhook.Name, webhook.Url)\n\treturn nil\n}\n\nfunc (webhook *WebhookAction) Send(content map[string]string) error {\n\tlog.Printf(\"Sending webhook to %q\", webhook.Url)\n\tdata := content[\"description\"] //it's not supposed to work with legacy renderer\n\tclient, err := newClient(webhook.Timeout)\n\tif err != nil {\n\t\treturn err\n\t}\n\tresp, err := client.Post(webhook.Url, \"application/json\", strings.NewReader(data))\n\tif err != nil {\n\t\tlog.Printf(\"Sending webhook Error: %v\", err)\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Sending %q Error: %v\", webhook.Name, err)\n\t\treturn err\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tmsg := \"Sending webhook wrong status: '%d'. Body: %s\"\n\t\tlog.Printf(msg, resp.StatusCode, body)\n\t\treturn fmt.Errorf(msg, resp.StatusCode, body)\n\t}\n\tlog.Printf(\"Sending Webhook to %q was successful!\", webhook.Name)\n\treturn nil\n}\n\nfunc (webhook *WebhookAction) Terminate() error {\n\tlog.Printf(\"Webhook action %q terminated.\", webhook.Name)\n\treturn nil\n}\n\nfunc (webhook *WebhookAction) GetLayoutProvider() layout.LayoutProvider {\n\t// Todo: This is MOCK. Because Formatting isn't need for Webhook\n\t// todo: The App should work with `return nil`\n\treturn new(formatting.HtmlProvider)\n}\n\nvar newClient = func(timeout string) (http.Client, error) {\n\tif len(timeout) == 0 || timeout == \"0\" {\n\t\ttimeout = \"120s\"\n\t}\n\tduration, err := time.ParseDuration(timeout)\n\tif err != nil {\n\t\treturn http.Client{}, fmt.Errorf(\"invalid duration specified: %w\", err)\n\t}\n\treturn http.Client{Timeout: duration}, nil\n}\n"
  },
  {
    "path": "actions/webhook_test.go",
    "content": "package actions\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWebhook_GetName(t *testing.T) {\n\twebhook := WebhookAction{Name: \"webhook action\"}\n\trequire.NoError(t, webhook.Init())\n\trequire.Equal(t, \"webhook action\", webhook.GetName())\n}\n\nfunc TestWebhook_Send(t *testing.T) {\n\ttype response = struct {\n\t\tstatus int\n\t\ttext   string\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\twebhook WebhookAction\n\t\tcontent map[string]string\n\t\tresp    response\n\t\twantErr string\n\t}{\n\t\t{\n\t\t\tname: \"happy path\",\n\t\t\twebhook: WebhookAction{\n\t\t\t\tName:    \"testName\",\n\t\t\t\tUrl:     \"%s/testUrl/webhook\",\n\t\t\t\tTimeout: \"120s\",\n\t\t\t},\n\t\t\tcontent: map[string]string{\n\t\t\t\t\"description\": \"test description\",\n\t\t\t},\n\t\t\tresp: response{\n\t\t\t\tstatus: http.StatusOK,\n\t\t\t\ttext:   \"OK\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"sad path (timeout error)\",\n\t\t\twebhook: WebhookAction{\n\t\t\t\tName:    \"testName\",\n\t\t\t\tUrl:     `%s/testUrl/webhook`,\n\t\t\t\tTimeout: \"0s\",\n\t\t\t},\n\t\t\tcontent: map[string]string{\n\t\t\t\t\"description\": \"test description\",\n\t\t\t},\n\t\t\tresp: response{\n\t\t\t\tstatus: http.StatusRequestTimeout,\n\t\t\t\ttext:   \"Timeout error\",\n\t\t\t},\n\t\t\twantErr: \"Sending webhook wrong status: '408'. Body: Timeout error\",\n\t\t},\n\t\t{\n\t\t\tname: \"sad path (Bad URL error)\",\n\t\t\twebhook: WebhookAction{\n\t\t\t\tName:    \"testName\",\n\t\t\t\tUrl:     \"badurl%s\",\n\t\t\t\tTimeout: \"1m\",\n\t\t\t},\n\t\t\tcontent: map[string]string{\n\t\t\t\t\"description\": \"test description\",\n\t\t\t},\n\t\t\twantErr: \"unsupported protocol scheme\",\n\t\t},\n\t}\n\n\tsavedNewClient := newClient\n\tdefer func() { newClient = savedNewClient }()\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\tw.WriteHeader(test.resp.status)\n\t\t\t\t_, _ = w.Write([]byte(test.resp.text))\n\t\t\t}))\n\t\t\tdefer server.Close()\n\n\t\t\ttest.webhook.Url = fmt.Sprintf(test.webhook.Url, server.URL)\n\n\t\t\tnewClient = func(timeout string) (http.Client, error) {\n\t\t\t\tclient := server.Client()\n\t\t\t\treturn *client, nil\n\t\t\t}\n\n\t\t\terr := test.webhook.Send(test.content)\n\n\t\t\tif test.wantErr != \"\" {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), test.wantErr)\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNewClient(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\ttimeout     string\n\t\twantTimeout time.Duration\n\t\twantError   string\n\t}{\n\t\t{\n\t\t\tname:        \"timeout 0\",\n\t\t\ttimeout:     \"0\",\n\t\t\twantTimeout: 120000000000,\n\t\t},\n\t\t{\n\t\t\tname:        \"timeout 60\",\n\t\t\ttimeout:     \"60s\",\n\t\t\twantTimeout: 60000000000,\n\t\t},\n\t\t{\n\t\t\tname:      \"bad timeout\",\n\t\t\ttimeout:   \"60sm\",\n\t\t\twantError: \"invalid duration specified\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tclient, err := newClient(test.timeout)\n\n\t\t\tif test.wantError != \"\" {\n\t\t\t\tassert.NotNil(t, err)\n\t\t\t\trequire.Contains(t, err.Error(), test.wantError)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, test.wantTimeout, client.Timeout)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cfg.yaml",
    "content": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant            #  The tenant name\naqua-server:            #  URL of Aqua Server for links. E.g. https://myserver.aquasec.com\nmax-db-size: 1000MB       #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\ndb-verify-interval: 1   #  How often to check the DB size. By default, Postee checks every 1 hour\n\n# Routes are used to define how to handle an incoming message\nroutes:\n- name: stdout\n  actions: [ stdout ]\n  template: raw-json\n\n#- name: route1                                 #  Route name. Must be unique\n#  input: contains(input.image, \"alpine\")       #  REGO rule to match input message against route\n#  input-files:                                  #  Array filePaths to files with REGO rules\n#   - Allow-Image-Name.rego\n#   - Ignore-Image-Name.rego\n#   - Allow-Registry.rego\n#   - Ignore-Registry.rego\n#   - Policy-Only-Fix-Available.rego\n#   - Policy-Min-Vulnerability.rego\n#   - Policy-Related-Features.rego\n#  actions: [my-slack]                          #  Action name (needs to be defined under \"actions\") which will receive the message\n#  template: slack-template                     #  Template name (needs to be defined under \"templates\") which will be used to process the message output format\n#  plugins:                                     #  Optional plugins\n#   aggregate-message-number:                   # Number of same messages to aggregate into one output message\n#   aggregate-message-timeout:                  # Number of seconds/minutes/hours to aggregate same messages into one output. Maximum is 24 hours. Use Xs or Xm or Xh\n#   unique-message-props: [\"digest\",\"image\",\"registry\", \"vulnerability_summary.high\", \"vulnerability_summary.medium\", \"vulnerability_summary.low\"] # Optional: Comma separated list of top level properties which uniqult identifies an event message. If message with same property values is received more than once it will be ignored\n#   unique-message-timeout:                     # Number of seconds/minutes/hours/days before expiring of a message. Expired messages are removed from db. If option is empty message is never deleted\n\n# Templates are used to format a message\ntemplates:\n- name: vuls-slack                  #  Out of the box template for slack\n  rego-package:  postee.vuls.slack      #  Slack template REGO package (available out of the box)\n- name: vuls-html                       #  Out of the box HTML template\n  rego-package:  postee.vuls.html       #  HTML template REGO package (available out of the box)\n- name: raw-html                        #  Raw message json\n  rego-package:  postee.rawmessage.html #  HTML template REGO package (available out of the box)\n- name: legacy                          #  Out of the box legacy Golang template\n  legacy-scan-renderer: html\n- name: legacy-slack                    #  Legacy slack template implemented in Golang\n  legacy-scan-renderer: slack\n- name: legacy-jira                     #  Legacy jira template implemented in Golang\n  legacy-scan-renderer: jira\n- name: custom-email                    #  Example of how to use a template from a Web URL\n  url:                                  #  URL to custom REGO file\n- name: raw-json                        # route message \"As Is\" to external webhook\n  rego-package: postee.rawmessage.json\n- name: vuls-cyclonedx                  # export vulnerabilities to CycloneDX XML\n  rego-package: postee.vuls.cyclondx\n- name: trivy-operator-jira\n  rego-package: postee.trivyoperator.jira\n- name: trivy-operator-slack\n  rego-package: postee.trivyoperator.slack\n- name: trivy-operator-dependencytrack\n  rego-package: postee.trivyoperator.dependencytrack\n- name: trivy-jira\n  rego-package: postee.trivy.jira\n\n# Rules are predefined rego policies that can be used to trigger routes\nrules:\n- name: Initial Access\n- name: Credential Access\n- name: Privilege Escalation\n- name: Defense Evasion\n- name: Persistence\n\n# Actions are target services that should consume the messages\nactions:\n- name: stdout\n  type: stdout\n  enable: true\n\n- name: my-jira   #  name must be unique\n  type: jira      #  supported types: jira, email\n  enable: false\n  url:            # Mandatory. E.g \"https://johndoe.atlassian.net\"\n  user:           # Mandatory. E.g :johndoe@gmail.com\"\n  password:       # Optional. Specify Jira user API key. Used only for Jira Cloud\n  token:          # Optional. Specify Jira user Personal Access Token. Used only for Jira Server/Data Center\n  project-key:    # Mandatory. Specify the JIRA product key\n  tls-verify: false\n  board:          # Optional. Specify the Jira board name to open tickets on\n  labels:         # Optional, specify array of labels to add to Ticket, for example: [\"label1\", \"label2\"]\n  issuetype:      # Optional. Specifty the issue type to open (Bug, Task, etc.). Default is \"Task\"\n  priority:       # Optional. Specify the issues severity. Default is \"High\"\n  assignee:       # Optional. Specify the assigned user. Default is the user that opened the ticket\n\n- name: my-email\n  type: email\n  enable: false\n  user:      # Optional (if auth supported): SMTP user name (e.g. johndoe@gmail.com)\n  password:  # Optional (if auth supported): SMTP password\n  host:      # Mandatory: SMTP host name (e.g. smtp.gmail.com)\n  port:      # Mandatory: SMTP server port (e.g. 587)\n  sender:    # Mandatory: The email address to use as a sender\n  client-host-name: # Optional: setting the local client name instead of `localhost`\n  recipients: [\"\", \"\"]  # Mandatory: comma separated list of recipients\n\n- name: my-email-smtp-server\n  type: email\n  enable: false\n  use-mx: true\n  sender:  # Mandatory: The email address to use as a sender\n  recipients: [\"\", \"\"]  # Mandatory: comma separated list of recipients\n    \n- name: my-slack\n  type: slack\n  enable: false\n  url: https://hooks.slack.com/services/TAAAA/BBB/<key>\n\n- name: ms-team\n  type: teams\n  enable: false\n  url: https://outlook.office.com/webhook/....   #  Webhook's url\n\n- name: webhook\n  type: webhook\n  enable: false\n  url: https://..../webhook/   #  Webhook's url\n  timeout:                     #  Webhook's timeout. <numbers><unit suffix> pattern is used, such as \"300ms\" or \"2h45m\". Default: 120s\n\n- name: splunk\n  type: splunk\n  enable: false\n  url: http://localhost:8088 # Mandatory. Url of a Splunk server\n  token: <token>             # Mandatory. a HTTP Event Collector Token\n  size-limit: 10000          # Optional. Maximum scan length, in bytes. Default: 10000\n  tls-verify: false          # Enable skip TLS Verification. Default: false.\n\n- name: my-servicenow\n  type: serviceNow\n  enable: false\n  user:      # Mandatory. E.g :johndoe@gmail.com\"\n  password:  # Mandatory. Specify user API key\n  instance:  # Mandatory. Name of ServiceN  ow Instance\n  board:     #  Specify the ServiceNow board name to open tickets on. Default is \"incident\"\n\n- name: my-nexus-iq\n  type: nexusIq\n  enable: false\n  user:             # Mandatory. User name\n  password:         # Mandatory. User password\n  url:              # Mandatory. Url of Nexus IQ server\n  organization-id:  # Mandatory. Organization UID like \"222de33e8005408a844c12eab952c9b0\"\n\n  - name: my-dependencytrack\n    type: dependencytrack\n    enable: false\n    url: http://localhost:8080/  # Mandatory. Url of Dependency Track server\n    dependency-track-api-key:    # Mandatory. API key of Dependency Track server\n\n- name: my-opsgenie\n  type: opsgenie\n  enable: false\n  token: <API Key>  # Mandatory. an API key from an API integration\n  user:             # Optional. Display name of the request owner.\n  assignee:         # Optional. Comma separated list of users that the alert will be routed to send notifications\n  recipients: [\"\"]  # Optional. Comma separated list of users that the alert will become visible to without sending any notification\n  tags:             # Optional. Comma separated list of the alert tags.\n  priority:         # Optional. Specify the alert priority. Default is \"P3\"\n  alias:            # Optional. Client-defined identifier of the alert.\n  entity:           # Optional. Entity field of the alert that is generally used to specify which domain alert is related to.\n"
  },
  {
    "path": "config/cfg-actions.yaml",
    "content": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant            #  The tenant name\naqua-server: localhost           #  URL of Aqua Server for links. E.g. https://myserver.aquasec.com\nmax-db-size: 1000MB       #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\ndb-verify-interval: 1   #  How often to check the DB size. By default, Postee checks every 1 hour\n\n# Routes are used to define how to handle an incoming message\nroutes:\n- name: stdout\n  actions: [ stdout ]\n  template: raw-json\n\n- name: actions-route\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  serialize-actions: true                       # Optional. Serialize actions in route.\n  actions: [my-exec, my-exec-2, my-http-get, my-http-post-file, my-http-post-content]\n  template: raw-json\n\n# Templates are used to format a message\ntemplates:\n- name: raw-json                        # route message \"As Is\" to external webhook\n  rego-package: postee.rawmessage.json\n\n# Outputs are target services that should consume the messages\nactions:\n- name: stdout\n  type: stdout\n  enable: true\n\n# Define a custom output of exec action, that can take params.\n- name: my-exec\n  type: exec\n  enable: true\n  env: [\"MY_ENV_VAR=foo_bar_baz\", \"MY_KEY=secret\"]     # Optional. Any environment variables to pass in\n  exec-script: |                                       # Specify the script to run\n    #!/bin/sh\n    echo $POSTEE_EVENT\n\n- name: my-exec-2\n  type: exec\n  enable: true\n  env: [\"MY_ENV_VAR=foo_bar_baz\", \"MY_KEY=secret\"]     # Optional. Any environment variables to pass in\n  input-file: /tmp/exec-test.sh                        # Specify the path to the script to run\n\n- name: my-http-get\n  type: http\n  enable: true\n  url: \"https://my-fancy-url.com\"     # Required. URL of the HTTP Request\n  method: GET                         # Required. Method to use. CONNECT is not supported at this time\n  headers:                            # Optional. Headers to pass in for the request\n    \"Foo\": [\"bar\", \"baz\"]\n  timeout: 1s                         # Optional. Timeout value in XX(s,m,h)\n\n- name: my-http-post-file\n  type: http\n  enable: true\n  url: \"https://my-fancy-url.com\"       # Required. URL of the HTTP Request\n  method: POST                          # Required. Method to use. CONNECT is not supported at this time\n  body-file: /tmp/some.log.file         # Optional. Body file of the HTTP request\n\n- name: my-http-post-content\n  type: http\n  enable: true\n  url: \"https://my-fancy-url.com\"       # Required. URL of the HTTP Request\n  method: POST                          # Required. Method to use. CONNECT is not supported at this time\n  headers:                              # Optional. Headers to pass in for the request.\n    \"Foo\": [ \"bar\" ]\n    \"Haz\": [ \"baz\" ]\n  timeout: 10s                          # Optional. Timeout value in XX(s,m,h)\n  body-content: |                       # Optional. Body inline content of the HTTP request\n    This is an example of a inline body\n    Event ID: event.input.Signature.ID"
  },
  {
    "path": "config/cfg-controller-runner.yaml",
    "content": "name: Postee Controller Runner Demo\n\naqua-server:            #  URL of Aqua Server for links. E.g. https://myserver.aquasec.com\nmax-db-size: 1000MB       #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\ndb-verify-interval: 1   #  How often to check the DB size. By default, Postee checks every 1 hour\n\n# Routes are used to define how to handle an incoming message\nroutes:\n- name: stdout\n  actions: [ stdout ]\n  template: raw-json\n\n- name: controller-only-route\n  input: contains(input.image, \"alpine\")\n  actions: [my-http-post-from-controller]\n  template: raw-json\n\n- name: runner-only-route\n  input: contains(input.SigMetadata.ID, \"TRC-1\")\n  serialize-actions: true\n  actions: [my-exec-from-runner, my-http-post-from-runner]\n  template: raw-json\n\n- name: controller-runner-route\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  serialize-actions: true     # Cannot be strictly guaranteed as executions happen independently on runner/controller\n  actions: [my-exec-from-runner, my-http-post-from-runner, my-http-post-from-controller]\n  template: raw-json\n\n# Templates are used to format a message\ntemplates:\n- name: raw-json                        # route message \"As Is\" to external webhook\n  rego-package: postee.rawmessage.json\n\n# Outputs are target services that should consume the messages\nactions:\n- name: stdout\n  type: stdout\n  enable: true\n\n- name: my-http-post-from-controller\n  type: http\n  enable: true\n  url: \"https://webhook.site/<uuid>\"       # Required. URL of the HTTP Request\n  method: POST                          # Required. Method to use. CONNECT is not supported at this time\n  headers:                              # Optional. Headers to pass in for the request.\n    \"Foo\": [ \"bar\" ]\n  timeout: 10s                          # Optional. Timeout value in XX(s,m,h)\n  body-content: |                       # Optional. Body inline content of the HTTP request\n    This is an example of a inline body\n    Input Image: event.input.image\n\n- name: my-exec-from-runner\n  runs-on: \"postee-runner-1\"\n  type: exec\n  enable: true\n  env: [\"MY_ENV_VAR=foo_bar_baz\", \"MY_KEY=secret\"]     # Optional. Any environment variables to pass in\n  exec-script: |                                       # Specify the script to run\n    #!/bin/sh\n    echo $POSTEE_EVENT\n    echo \"this is hello from postee\"\n\n- name: my-http-post-from-runner\n  runs-on: \"postee-runner-1\"\n  type: http\n  enable: true\n  url: \"https://webhook.site/<uuid>\"       # Required. URL of the HTTP Request\n  method: POST                          # Required. Method to use. CONNECT is not supported at this time\n  body-content: |                       # Optional. Body inline content of the HTTP request\n    This is an another example of a inline body\n    Event ID: event.input.SigMetadata.ID\n"
  },
  {
    "path": "config/cfg-docker-actions.yaml",
    "content": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant            #  The tenant name\naqua-server:            #  URL of Aqua Server for links. E.g. https://myserver.aquasec.com\nmax-db-size: 1000MB       #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\ndb-verify-interval: 1   #  How often to check the DB size. By default, Postee checks every 1 hour\n\n# Routes are used to define how to handle an incoming message\nroutes:\n- name: stdout\n  actions: [ stdout ]\n  template: raw-json\n\n- name: actions-route\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  actions: [stop-vulnerable-pod]\n  template: raw-json\n\n# Templates are used to format a message\ntemplates:\n- name: raw-json                                   # route message \"As Is\" to external webhook\n  rego-package: postee.rawmessage.json\n\n# Outputs are target services that should consume the messages\nactions:\n- name: stdout\n  type: stdout\n  enable: true\n\n# Define a custom output of Docker action, that can take params.\n- name: stop-vulnerable-pod\n  type: docker\n  enable: true\n  docker-image-name: \"bitnami/kubectl:latest\"                          # Required. Image name of container to run.\n  docker-cmd: [\"delete\", \"pod\", event.input.SigMetadata.hostname]      # Required. Command to run when starting container.\n  docker-network: \"host\"                                               # Optional. Network name for docker container.\n  docker-volume-mounts:                                                # Optional. Volume mounts present inside the container\n    \"path/to/.kube/config\": \"/.kube/config\""
  },
  {
    "path": "config/cfg-k8s-actions.yaml",
    "content": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant            #  The tenant name\naqua-server:            #  URL of Aqua Server for links. E.g. https://myserver.aquasec.com\nmax-db-size: 1000MB       #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\ndb-verify-interval: 1   #  How often to check the DB size. By default, Postee checks every 1 hour\n\n# Routes are used to define how to handle an incoming message\nroutes:\n- name: stdout\n  actions: [ stdout ]\n  template: raw-json\n\n- name: actions-route\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  actions: [my-k8s]\n  template: raw-json\n\n# Templates are used to format a message\ntemplates:\n- name: raw-json                                   # route message \"As Is\" to external webhook\n  rego-package: postee.rawmessage.json\n\n# Outputs are target services that should consume the messages\nactions:\n- name: stdout\n  type: stdout\n  enable: true\n\n# Define a custom output of k8s action, that can take params.\n- name: my-k8s\n  type: kubernetes\n  enable: true\n  kube-namespace: \"default\"                         # Required. Kubernetes namespace to use.\n  kube-config-file: \"/path/to/kubeconfig\"           # Required if not running on K8S, Optional otherwise.\n  kube-label-selector: \"app=nginx-app\"              # Required, if specifying labels or annotations.\n  kube-actions:\n    labels:\n      foo-label: \"bar-value\"                        # Required. Label to add.\n      bar-label: event.input.SigMetadata.ID         # Optional. It is also possible to add labels based on event inputs.\n    annotations:\n      foo-annotation: \"bar-value\"\n      bar-annotation: event.input.SigMetadata.ID"
  },
  {
    "path": "config/cfg-trivy-aws.yaml",
    "content": "actions:\n  - type: awssecurityhub\n    enable: true\n    name: Send Findings to Security Hub\nroutes:\n  - name: Send Trivy Findings to AWS Security Hub\n    template: raw-json\n    actions:\n      - Send Findings to Security Hub\n    input-files:\n      - Trivy AWS Findings\ntemplates:\n  - name: raw-json\n    rego-package: postee.rawmessage.json\nrules:\n  - name: Trivy AWS Findings\nname: Send Trivy Results to AWS Security Hub\n"
  },
  {
    "path": "config/cfg-trivy-operator-defectdojo.yaml",
    "content": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant            #  The tenant name\naqua-server:            #  URL of Aqua Server for links. E.g. https://myserver.aquasec.com\nmax-db-size: 1000MB       #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\ndb-verify-interval: 1   #  How often to check the DB size. By default, Postee checks every 1 hour\n\n# Routes are used to define how to handle an incoming message\nroutes:\n- name: trivyScans\n  actions: [ exec-curl-dd ]\n  template: plejd-dd-trivy-report\n  input: contains(input.kind, \"ClusterRbacAssessmentReport\")\n\n# Actions are target services that should consume the messages\nactions:\n- name: stdout\n  type: stdout\n  enable: false\n\n- name: exec-curl-dd           # use curl to send message to defectdojo\n  type: exec\n  enable: true\n  env:\n    - \"DEFECTDOJO_URL=http://webserver-http-echo:8888\"\n  input-file: /tmp/exec-curl-dd.sh       # action expects shell script (plain sh shell)\n\ntemplates:\n- name: raw-json                        # route message \"As Is\" to external webhook\n  rego-package: postee.rawmessage.json\n\n- name: plejd-dd-trivy-report           # render from report into DD structure\n  rego-package: plejd.trivyoperator.defectdojo\n"
  },
  {
    "path": "config/cfg-trivy-operator.yaml",
    "content": "routes:\n- name: Trivy Operator Alerts\n  input: input.report.summary.criticalCount > 0 # You can customize this based on your needs\n  actions: [send-slack-msg]\n  template: trivy-operator-slack\n\n# Templates are used to format a message\ntemplates:\n- name: trivy-operator-slack\n  rego-package: postee.trivyoperator.slack\n\n# Actions are target services that should consume the messages\nactions:\n- name: send-slack-msg\n  type: slack\n  enable: true\n  url: <slack webhook url>\n"
  },
  {
    "path": "config/terminate-malicious-pods.yaml",
    "content": "actions:\n  - type: webhook\n    name: Send Message to Webhook\n    enable: true\n    url: http://foo.com\n  - type: exec\n    name: Kill Process\n    enable: true\n    input-file: ''\n    exec-script: kill -9 $(echo $POSTEE_EVENT | jq .Context.hostParentProcessId)\nroutes:\n  - name: Notify on unauthorized access\n    actions:\n      - Send Message to Webhook\n    template: raw-json\n    action: []\n    input-files:\n      - Privilege Escalation\n      - Defense Evasion\n      - Credential Access\n      - Initial Access\n  - action: []\n    input-files:\n      - Defense Evasion\n    actions:\n      - Kill Process\n      - Send Message to Webhook\n    name: Terminate offending process\n    template: raw-json\ntemplates:\n  - name: raw-json\n    rego-package: postee.rawmessage.json\nrules:\n  - name: Initial Access\n  - name: Credential Access\n  - name: Privilege Escalation\n  - name: Defense Evasion\n  - name: Persistence\n  - name: Tracee Default Set\nname: Terminating Malicious Pods\n"
  },
  {
    "path": "controller/controller.go",
    "content": "package controller\n\nimport (\n\tgotls \"crypto/tls\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/router\"\n\t\"github.com/nats-io/nats-server/v2/server\"\n\t\"github.com/nats-io/nats.go\"\n\t\"github.com/nats-io/nkeys\"\n)\n\nconst (\n\tNATSEventSubject  = \"postee.events\"\n\tNATSConfigSubject = \"postee.config\"\n)\n\ntype Controller struct {\n\tControllerURL          string\n\tControllerSeedFilePath string\n\tControllerCAFile       string\n\tControllerTLSKeyPath   string\n\tControllerTLSCertPath  string\n\tRunnerName             string\n}\n\nfunc (c Controller) Setup(r *router.Router) error {\n\tlog.Println(\"Running in controller mode\")\n\n\tvar configCh chan *nats.Msg\n\tvar natsServer *server.Server\n\n\tvar host string\n\tvar port int\n\tif c.ControllerURL != \"\" {\n\t\tvar portString string\n\t\tu, err := url.Parse(c.ControllerURL)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid controller url specified: %s\", c.ControllerURL)\n\t\t}\n\t\thost, portString, _ = net.SplitHostPort(u.Host)\n\t\tport, _ = strconv.Atoi(portString)\n\t}\n\n\tvar err error\n\tif c.ControllerTLSKeyPath != \"\" && c.ControllerTLSCertPath != \"\" {\n\t\tvar tlsConfig *gotls.Config\n\t\ttlsConfig, err = server.GenTLSConfig(&server.TLSConfigOpts{\n\t\t\tCertFile: c.ControllerTLSCertPath,\n\t\t\tKeyFile:  c.ControllerTLSKeyPath,\n\t\t\tCaFile:   c.ControllerCAFile,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid TLS config: %w\", err)\n\t\t}\n\n\t\tvar pubKey string\n\t\tvar nKeys []*server.NkeyUser\n\t\tif c.ControllerSeedFilePath != \"\" {\n\t\t\tlog.Println(\"Seedfile specified for Controller, enabling AuthN\")\n\t\t\tsf, err := ioutil.ReadFile(c.ControllerSeedFilePath)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to read seed file: %w\", err)\n\t\t\t}\n\n\t\t\tnKey, err := nkeys.ParseDecoratedNKey(sf)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to parse seed file: %w\", err)\n\t\t\t}\n\n\t\t\tpubKey, err = nKey.PublicKey()\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"unable to get public key: %w\", err)\n\t\t\t}\n\n\t\t\tnKeys = append(nKeys, &server.NkeyUser{Nkey: pubKey})\n\t\t}\n\n\t\tnatsServer, err = server.NewServer(&server.Options{\n\t\t\tTLSConfig: tlsConfig,\n\t\t\tNkeys:     nKeys,\n\t\t\tHost:      host,\n\t\t\tPort:      port,\n\t\t})\n\t} else {\n\t\tnatsServer, err = server.NewServer(&server.Options{Host: host, Port: port})\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to start controller backplane: %w\", err)\n\t}\n\tgo natsServer.Start()\n\tif !natsServer.ReadyForConnections(time.Second * 10) {\n\t\treturn fmt.Errorf(\"controller backplane is not ready to receive connections, try restarting controller\")\n\t}\n\n\tlog.Println(\"Controller listening for requests on: \", natsServer.ClientURL())\n\tconfigCh = make(chan *nats.Msg)\n\n\tvar opts []nats.Option\n\tvar nKeyOpt nats.Option\n\tif c.ControllerSeedFilePath != \"\" {\n\t\tnKeyOpt, err = nats.NkeyOptionFromSeed(c.ControllerSeedFilePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to load seed file: %w\", err)\n\t\t}\n\t}\n\topts = append(opts, nKeyOpt)\n\n\tvar nc *nats.Conn\n\tif c.ControllerCAFile != \"\" {\n\t\topts = append(opts, nats.RootCAs(c.ControllerCAFile))\n\t}\n\tnc, err = nats.Connect(natsServer.ClientURL(), router.SetupConnOptions(opts)...)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to setup controller: %w\", err)\n\t}\n\n\tlog.Println(\"Listening to config requests on: \", NATSConfigSubject)\n\tif _, err := nc.ChanSubscribe(NATSConfigSubject, configCh); err != nil {\n\t\treturn fmt.Errorf(\"unable to subscribe for config requests from runners on: %s, err: %w\", NATSConfigSubject, err)\n\t}\n\n\tr.ConfigCh = configCh\n\tr.NatsServer = natsServer\n\tr.Mode = \"controller\"\n\n\tr.NatsMsgCh = make(chan *nats.Msg)\n\teventSubj := NATSEventSubject\n\tlog.Println(\"Subscribing to events from runners on: \", eventSubj)\n\tif _, err := nc.ChanSubscribe(eventSubj, r.NatsMsgCh); err != nil {\n\t\treturn fmt.Errorf(\"unable to subscribe for events from runners on: %s, err: %w\", eventSubj, err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "data/inpteval.go",
    "content": "package data\n\ntype Inpteval interface {\n\tEval(in map[string]interface{}, serverUrl string) (map[string]string, error)\n\tBuildAggregatedContent(items []map[string]string) (map[string]string, error)\n\tIsAggregationSupported() bool\n}\n"
  },
  {
    "path": "data/slack.go",
    "content": "package data\n\ntype SlackTextBlock struct {\n\tTypeField string `json:\"type\"`\n\tTextField string `json:\"text\"`\n}\n\ntype SlackBlock struct {\n\tTypeField string           `json:\"type\"`\n\tTextField *SlackTextBlock  `json:\"text,omitempty\"`\n\tFields    []SlackTextBlock `json:\"fields,omitempty\"`\n}\n"
  },
  {
    "path": "data/types.go",
    "content": "package data\n\ntype ScanImageInfo struct {\n\tImage                  string `json:\"image\"`\n\tRegistry               string `json:\"registry\"`\n\tDigest                 string `json:\"digest\"`\n\tPreviousDigest         string `json:\"previous_digest\"`\n\tImageAssuranceResults  `json:\"image_assurance_results,omitempty\"`\n\tVulnerabilitySummary   `json:\"vulnerability_summary,omitempty\"`\n\tScanOptions            `json:\"scan_options,omitempty\"`\n\tResources              []InfoResources `json:\"resources,omitempty\"`\n\tApplicationScopeOwners []string        `json:\"application_scope_owners,omitempty\"`\n\tMalwares               []MalwareData   `json:\"malware,omitempty\"`\n\tSensitiveData          []SensitiveData `json:\"sensitive_data,omitempty\"`\n}\n\ntype SensitiveData struct {\n\tFilename string `json:\"filename\"`\n\tPath     string `json:\"path\"`\n\tType     string `json:\"type\"`\n\tHash     string `json:\"hash\"`\n}\n\ntype MalwareData struct {\n\tMalware string `json:\"malware\"`\n\tPath    string `json:\"path\"`\n\tHash    string `json:\"hash\"`\n}\n\ntype ImageAssuranceResults struct {\n\tDisallowed      bool           `json:\"disallowed\"`\n\tChecksPerformed []ControlCheck `json:\"checks_performed\"`\n}\n\ntype ControlCheck struct {\n\tControl    string `json:\"control\"`\n\tPolicyName string `json:\"policy_name\"`\n\tFailed     bool   `json:\"failed\"`\n}\n\ntype ScanOptions struct {\n\tScanSensitiveData bool `json:\"scan_sensitive_data\"`\n\tScanMalware       bool `json:\"scan_malware\"`\n}\n\ntype VulnerabilitySummary struct {\n\tTotal      int `json:\"total\"`\n\tCritical   int `json:\"critical\"`\n\tHigh       int `json:\"high\"`\n\tMedium     int `json:\"medium\"`\n\tLow        int `json:\"low\"`\n\tNegligible int `json:\"negligible\"`\n\tSensitive  int `json:\"sensitive\"`\n\tMalware    int `json:\"malware\"`\n}\n\ntype InfoResources struct {\n\tVulnerabilities []Vulnerability `json:\"vulnerabilities\"`\n\tResourceDetails `json:\"resource\"`\n}\ntype ResourceDetails struct {\n\tName    string `json:\"name\"`\n\tVersion string `json:\"version\"`\n}\n\ntype Vulnerability struct {\n\tName       string `json:\"name\"`\n\tVersion    string `json:\"version\"`\n\tFixVersion string `json:\"fix_version\"`\n\tSeverity   string `json:\"aqua_severity\"` //`json:\"\"`nvd_severity\n}\n"
  },
  {
    "path": "data/utils.go",
    "content": "package data\n\nimport (\n\t\"regexp\"\n)\n\nfunc ClearField(source string) string {\n\tre := regexp.MustCompile(`[[:cntrl:]]|[\\x{FFFD}]`)\n\treturn re.ReplaceAllString(source, \"\")\n}\n"
  },
  {
    "path": "data/utils_test.go",
    "content": "package data\n\nimport (\n\t\"testing\"\n)\n\nfunc TestClearField(t *testing.T) {\n\ttests := []struct {\n\t\tin, out string\n\t}{\n\t\t{\"test\\r\", \"test\"},\n\t\t{\"test\\n\", \"test\"},\n\t\t{\"the\\xFF \\xFDtest\", \"the test\"},\n\t}\n\n\tfor _, test := range tests {\n\t\tif got := ClearField(test.in); got != test.out {\n\t\t\tt.Errorf(\"ClearField(%q) == %q, want %q\", test.in, got, test.out)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "dbservice/actions.go",
    "content": "package dbservice\n\nimport (\n\t\"time\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc MayBeStoreMessage(message []byte, messageKey string, expired *time.Time) (wasStored bool, err error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\n\tdb, err := bolt.Open(DbPath, 0666, nil)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer db.Close()\n\n\tif err = Init(db, dbBucketName); err != nil {\n\t\treturn false, err\n\t}\n\tif err = Init(db, dbBucketExpiryDates); err != nil {\n\t\treturn false, err\n\t}\n\n\tcurrentValue, err := dbSelect(db, dbBucketName, messageKey)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif currentValue != nil {\n\t\treturn false, nil\n\t} else {\n\t\tbMessageKey := []byte(messageKey)\n\t\terr = dbInsert(db, dbBucketName, bMessageKey, message)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\t\tif expired != nil {\n\t\t\terr = dbInsert(db, dbBucketExpiryDates, []byte(expired.Format(DateFmt)), bMessageKey)\n\t\t\tif err != nil {\n\t\t\t\treturn false, err\n\t\t\t}\n\t\t}\n\t\treturn true, nil\n\t}\n\n}\n"
  },
  {
    "path": "dbservice/changedbpath_test.go",
    "content": "package dbservice\n\nimport \"testing\"\n\nfunc TestChangeDbPath(t *testing.T) {\n\ttestPath := \"/tmp/test.db\"\n\tstoredPath := DbPath\n\tChangeDbPath(testPath)\n\tdefer func() {\n\t\tChangeDbPath(storedPath)\n\t}()\n\tif DbPath != testPath {\n\t\tt.Errorf(\"path is not configured correctly, expected: %s, got %s\", testPath, DbPath)\n\t}\n}\n"
  },
  {
    "path": "dbservice/checker.go",
    "content": "package dbservice\n\nimport (\n\t\"bytes\"\n\t\"github.com/aquasecurity/postee/v2/utils\"\n\t\"log\"\n\t\"time\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc CheckSizeLimit() {\n\tif DbSizeLimit == 0 {\n\t\treturn\n\t}\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\n\tdb, err := bolt.Open(DbPath, 0666, nil)\n\tif err != nil {\n\t\tlog.Println(\"CheckSizeLimit: Can't open db:\", DbPath)\n\t\treturn\n\t}\n\tdefer db.Close()\n\n\tif err := db.Update(func(tx *bolt.Tx) error {\n\t\tb := tx.Bucket([]byte(dbBucketName))\n\t\tif b == nil {\n\t\t\treturn nil\n\t\t}\n\t\tc := b.Cursor()\n\t\tsize := 0\n\t\tfor k, v := c.First(); k != nil; k, v = c.Next() {\n\t\t\tsize += len(v)\n\t\t}\n\t\tif size > DbSizeLimit {\n\t\t\terr = tx.DeleteBucket([]byte(dbBucketName))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\terr = tx.DeleteBucket([]byte(dbBucketExpiryDates))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tutils.Debug(\"DB size: %db is over size limit: %db, DB is cleared\", size, DbSizeLimit)\n\t\t}\n\t\treturn nil\n\t}); err != nil {\n\t\tlog.Println(\"Error a check of db size:\", err)\n\t\treturn\n\t}\n}\n\nfunc CheckExpiredData() {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\n\tdb, err := bolt.Open(DbPath, 0666, nil)\n\tif err != nil {\n\t\tlog.Println(\"CheckExpiredData: Can't open db:\", DbPath)\n\t\treturn\n\t}\n\tdefer db.Close()\n\n\texpired, err := getExpired(db)\n\tif err != nil {\n\t\tlog.Println(\"Can't select expired data: \", err)\n\t\treturn\n\t}\n\n\tif err := dbDelete(db, dbBucketName, expired); err != nil {\n\t\tlog.Println(\"Can't remove expired data: \", err)\n\t}\n}\n\nfunc getExpired(db *bolt.DB) (keys [][]byte, err error) {\n\tkeys = [][]byte{}\n\tttlKeys := [][]byte{}\n\n\tif err = db.View(func(tx *bolt.Tx) error {\n\t\tb := tx.Bucket([]byte(dbBucketExpiryDates))\n\t\tif b == nil {\n\t\t\treturn nil\n\t\t}\n\t\tc := b.Cursor()\n\n\t\tmax := []byte(time.Now().UTC().Format(DateFmt)) //remove expired records\n\t\tfor k, v := c.First(); k != nil && bytes.Compare(k, max) <= 0; k, v = c.Next() {\n\t\t\tkeys = append(keys, v)\n\t\t\tttlKeys = append(ttlKeys, k)\n\t\t}\n\t\treturn nil\n\t}); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = dbDelete(db, dbBucketExpiryDates, ttlKeys); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "dbservice/checker_test.go",
    "content": "package dbservice\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc TestExpiredDates(t *testing.T) {\n\tdbPathReal := DbPath\n\trealDueTimeBase := dueTimeBase\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t\tdueTimeBase = realDueTimeBase\n\t}()\n\tdueTimeBase = time.Nanosecond\n\tDbPath = \"test_webhooks.db\"\n\ttests := []struct {\n\t\ttitle                       string\n\t\tdelay                       int\n\t\tuniqueMessageTimeoutSeconds int\n\t\tneedRun                     bool\n\t\twasStored                   bool\n\t}{\n\t\t{\"Add initial scan\", 0, 1, false, true},\n\t\t{\"Add same scan again - not stored\", 0, 0, true, false},\n\t\t{\"Add same scan again - after delay - stored\", 1, 0, true, true},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Log(test.title)\n\t\tif test.needRun {\n\t\t\ttime.Sleep(time.Duration(test.delay) * time.Second)\n\t\t\tCheckExpiredData()\n\t\t}\n\t\ttimeToExpire := time.Duration(test.uniqueMessageTimeoutSeconds) * time.Second\n\t\texpired := time.Now().UTC().Add(timeToExpire)\n\n\t\twasStored, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, &expired)\n\n\t\tif err != nil {\n\t\t\tt.Fatal(\"First Add AlpineImageResult Error\", err)\n\t\t}\n\n\t\tif wasStored != test.wasStored {\n\t\t\tt.Errorf(\"Error handling! Want wasStored: %t, got: %t\", test.wasStored, wasStored)\n\t\t}\n\t}\n}\n\nfunc TestCheckSizeLimit(t *testing.T) {\n\tdbPathReal := DbPath\n\trealSizeLimit := DbSizeLimit\n\tdefer func() {\n\t\tDbPath = dbPathReal\n\t\tDbSizeLimit = realSizeLimit\n\t}()\n\n\tDbPath = \"test_webhooks.db\"\n\n\ttests := []struct {\n\t\tname        string\n\t\tdbSizeLimit int\n\t\twasCleared  bool\n\t}{\n\t\t{\n\t\t\tname:        \"DB has been cleared\",\n\t\t\tdbSizeLimit: 1,\n\t\t\twasCleared:  true,\n\t\t},\n\t\t{\n\t\t\tname:       \"DB not cleared\",\n\t\t\twasCleared: false,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tDbSizeLimit = test.dbSizeLimit\n\n\t\t\tdb, err := bolt.Open(DbPath, 0666, nil)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"Can't open db:\", DbPath)\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\tos.Remove(DbPath)\n\t\t\t\tdb.Close()\n\t\t\t}()\n\n\t\t\terr = dbInsert(db, dbBucketName, []byte(\"sha256:12345\"), []byte(\"input_struct\"))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"TestDbDelete dbInsert: \", err)\n\t\t\t}\n\n\t\t\terr = dbInsert(db, dbBucketExpiryDates, []byte(\"2222-02-22T04:37:25.251356543Z\"), []byte(\"sha256:12345\"))\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(\"TestDbDelete dbInsert: \", err)\n\t\t\t}\n\n\t\t\terr = db.Close() // CheckSizeLimit() will open DB. We must close DB before doing this.\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"unable close DB: %v\", err)\n\t\t\t}\n\n\t\t\tCheckSizeLimit()\n\n\t\t\texistDbBucketName, err := dbBucketExists(db, dbBucketName)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Unable to check if bucket exists: %v\", err)\n\t\t\t}\n\n\t\t\texistDbBucketExpiryDates, err := dbBucketExists(db, dbBucketExpiryDates)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Unable to check if bucket exists: %v\", err)\n\t\t\t}\n\n\t\t\tif test.wasCleared {\n\t\t\t\tassert.False(t, existDbBucketName)\n\t\t\t\tassert.False(t, existDbBucketExpiryDates)\n\t\t\t} else {\n\t\t\t\tassert.True(t, existDbBucketName)\n\t\t\t\tassert.True(t, existDbBucketExpiryDates)\n\t\t\t}\n\n\t\t})\n\t}\n}\n\nfunc TestWrongBuckets(t *testing.T) {\n\tsavedDbBucketName := dbBucketName\n\tsavedDbBucketExpiryDates := dbBucketExpiryDates\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tdbBucketName = savedDbBucketName\n\t\tdbBucketExpiryDates = savedDbBucketExpiryDates\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\n\t_, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tDbSizeLimit = 1\n\tdbBucketName = \"\"\n\tdbBucketExpiryDates = \"\"\n\tCheckSizeLimit()\n\n\tdbBucketName = \"dbBucketName\"\n\t_, err = MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil)\n\tif err == nil {\n\t\tt.Error(\"No error for empty dbBucketExpiryDates\")\n\t}\n\tdbBucketExpiryDates = \"dbBucketExpiryDates\"\n\tdbBucketName = \"\"\n\t_, err = MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil)\n\tif err == nil {\n\t\tt.Error(\"No error for empty dbBucketName\")\n\t}\n}\n\nfunc TestDbDelete(t *testing.T) {\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\n\tdb, err := bolt.Open(DbPath, 0666, nil)\n\tif err != nil {\n\t\tt.Fatal(\"Can't open db:\", DbPath)\n\t}\n\tdefer db.Close()\n\n\tkey := []byte(\"key\")\n\tvalue := []byte(\"value\")\n\tbucket := \"b\"\n\n\terr = dbInsert(db, bucket, key, value)\n\tif err != nil {\n\t\tt.Fatal(\"TestDbDelete dbInsert: \", err)\n\t}\n\terr = dbDelete(db, bucket, [][]byte{key})\n\tif err != nil {\n\t\tt.Fatal(\"TestDbDelete dbInsert: \", err)\n\t}\n\n\terr = db.Close()\n\tif err != nil {\n\t\tt.Errorf(\"Unable close DB: %v\", err)\n\t}\n\n\texist, err := dbBucketExists(db, bucket)\n\tif err != nil {\n\t\tt.Errorf(\"Unable to check if bucket exists: %v\", err)\n\t}\n\n\tif !exist {\n\t\tt.Errorf(\"bucket hasn't been removed \")\n\t}\n}\n\nfunc TestWithoutAccessToDb(t *testing.T) {\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\tdb, err := bolt.Open(DbPath, 0220, nil)\n\tif err != nil {\n\t\tt.Fatal(\"Can't open db:\", DbPath)\n\t}\n\tdb.Close()\n\tDbSizeLimit = 1\n\tCheckSizeLimit()\n\tCheckExpiredData()\n}\n\nfunc dbBucketExists(db *bolt.DB, bucket string) (bool, error) {\n\tbucketExist := false\n\n\tdb, err := bolt.Open(DbPath, 0666, nil)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer func() {\n\t\tdb.Close()\n\t}()\n\n\terr = db.View(func(tx *bolt.Tx) error {\n\t\tb := tx.Bucket([]byte(bucket))\n\t\tif b != nil {\n\t\t\tbucketExist = true\n\t\t\treturn nil\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn bucketExist, nil\n}\n"
  },
  {
    "path": "dbservice/dbaggregator.go",
    "content": "package dbservice\n\nimport (\n\t\"encoding/json\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc AggregateScans(output string,\n\tcurrentScan map[string]string,\n\tscansPerTicket int,\n\tignoreTheQuantity bool) ([]map[string]string, error) {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\n\tdb, err := bolt.Open(DbPath, 0666, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer db.Close()\n\n\terr = Init(db, dbBucketAggregator)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\taggregatedScans := make([]map[string]string, 0, scansPerTicket)\n\tif len(currentScan) > 0 {\n\t\taggregatedScans = append(aggregatedScans, currentScan)\n\t}\n\tcurrentValue, err := dbSelect(db, dbBucketAggregator, output)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(currentValue) > 0 {\n\t\tvar savedScans []map[string]string\n\t\terr = json.Unmarshal(currentValue, &savedScans)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\taggregatedScans = append(aggregatedScans, savedScans...)\n\t}\n\n\tif ignoreTheQuantity || len(aggregatedScans) < scansPerTicket {\n\t\tsaving, err := json.Marshal(aggregatedScans)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\terr = dbInsert(db, dbBucketAggregator, []byte(output), saving)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn nil, nil\n\t}\n\terr = dbInsert(db, dbBucketAggregator, []byte(output), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn aggregatedScans, nil\n}\n"
  },
  {
    "path": "dbservice/dbaggregator_test.go",
    "content": "package dbservice\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestAggregateScans(t *testing.T) {\n\tvar (\n\t\tscan1 = map[string]string{\"title\": \"t1\", \"description\": \"d1\"}\n\t\tscan2 = map[string]string{\"title\": \"t2\", \"description\": \"d2\"}\n\t\tscan3 = map[string]string{\"title\": \"t3\", \"description\": \"d3\"}\n\t\tscan4 = map[string]string{\"title\": \"t4\", \"description\": \"d4\"}\n\t)\n\n\tvar tests = [...]struct {\n\t\toutput         string\n\t\tcurrentScan    map[string]string\n\t\tscansPerTicket int\n\t\twant           []map[string]string\n\t}{\n\t\t{\n\t\t\t\"jira\",\n\t\t\tscan1,\n\t\t\t3,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"jira\",\n\t\t\tscan2,\n\t\t\t3,\n\t\t\tnil,\n\t\t},\n\t\t{\n\t\t\t\"jira\",\n\t\t\tscan3,\n\t\t\t3,\n\t\t\t[]map[string]string{scan3, scan2, scan1},\n\t\t},\n\t\t{\n\t\t\t\"jira\",\n\t\t\tscan4,\n\t\t\t3,\n\t\t\tnil,\n\t\t},\n\t}\n\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\n\tfor i := 0; i < len(tests); i++ {\n\t\ttest := tests[i]\n\t\taggregated, err := AggregateScans(test.output, test.currentScan, test.scansPerTicket, false)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"AggregateScans Error: %v\", err)\n\t\t\tcontinue\n\t\t}\n\n\t\tif len(aggregated) != len(test.want) {\n\t\t\tt.Errorf(\"Wrong result size\\nResult: %v\\nWaited: %v\", aggregated, test.want)\n\t\t\tcontinue\n\t\t}\n\n\t\tfor i := 0; i < len(aggregated); i++ {\n\t\t\tif aggregated[i][\"title\"] != test.want[i][\"title\"] {\n\t\t\t\tt.Errorf(\"Wrong title\\nResult: %q\\nWaited: %q\", aggregated[i][\"title\"], test.want[i][\"title\"])\n\t\t\t}\n\t\t\tif aggregated[i][\"description\"] != test.want[i][\"description\"] {\n\t\t\t\tt.Errorf(\"Wrong Description\\nResult: %q\\nWaited: %q\", aggregated[i][\"description\"], test.want[i][\"description\"])\n\t\t\t}\n\t\t}\n\t}\n\n\t// Test of existence last scan in DB\n\tlastScan, err := AggregateScans(\"jira\", nil, 0, false)\n\tif err != nil {\n\t\tt.Fatalf(\"AggregateScans Error: %v\", err)\n\t}\n\n\tif len(lastScan) != 1 {\n\t\tt.Fatalf(\"Db don't contain last scan\")\n\t}\n\n\tif lastScan[0][\"title\"] != scan4[\"title\"] {\n\t\tt.Errorf(\"Wrong title\\nResult: %q\\nWaited: %q\", lastScan[0][\"title\"], scan4[\"title\"])\n\t}\n\tif lastScan[0][\"description\"] != scan4[\"description\"] {\n\t\tt.Errorf(\"Wrong Description\\nResult: %q\\nWaited: %q\", lastScan[0][\"description\"], scan4[\"description\"])\n\t}\n}\n"
  },
  {
    "path": "dbservice/dbparam.go",
    "content": "package dbservice\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar (\n\tdbBucketName         = \"WebhookBucket\"\n\tdbBucketAggregator   = \"WebhookAggregator\"\n\tdbBucketExpiryDates  = \"WebookExpiryDates\"\n\tDbBucketActionStats  = \"WebhookActionStats\"\n\tDbBucketSharedConfig = \"WebhookSharedConfig\"\n\n\tDbSizeLimit = 0\n\tdueTimeBase = time.Hour * time.Duration(24)\n\tDateFmt     = time.RFC3339Nano\n\n\tDbPath = \"/server/database/webhooks.db\"\n\tmutex  sync.Mutex\n)\n\nfunc ChangeDbPath(newPath string) {\n\tmutex.Lock()\n\tDbPath = newPath\n\tmutex.Unlock()\n}\n\nfunc SetNewDbPathFromEnv() {\n\tnewPath := os.Getenv(\"PATH_TO_DB\")\n\tif newPath != \"\" {\n\t\tif _, err := os.Stat(newPath); err != nil {\n\t\t\tif os.IsNotExist(err) {\n\t\t\t\terr = os.MkdirAll(filepath.Dir(newPath), os.ModePerm)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Printf(\"Can't create DateBase directory: %v, the default path is used\", err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"Can't check DateBase directory: %v, the default path is used\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tChangeDbPath(newPath)\n\t}\n}\n"
  },
  {
    "path": "dbservice/dbparam_test.go",
    "content": "package dbservice\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestSetNewDbPathFromEnv(t *testing.T) {\n\tenvPathToDbOld := os.Getenv(\"PATH_TO_DB\")\n\tdefer os.Setenv(\"PATH_TO_DB\", envPathToDbOld)\n\tdbPathOld := DbPath\n\n\tdefaultDbPath := \"/server/database/webhooks.db\"\n\tvar tests = []struct {\n\t\tname             string\n\t\tenvPathToDb      string\n\t\tchangePermission bool\n\t\texpectedDBPath   string\n\t}{\n\t\t{\"Empty PATH_TO_DB\", \"\", false, defaultDbPath},\n\t\t{\"Permission denied to create directory(default DbPath is used)\", \"/database/database.db\", false, defaultDbPath},\n\t\t{\"New DbPath\", \"./base/base.db\", false, \"./base/base.db\"},\n\t\t{\"Permission denied to check directory(default DbPath is used)\", \"webhook/database/webhooks.db\", true, defaultDbPath},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tos.Setenv(\"PATH_TO_DB\", test.envPathToDb)\n\t\t\tbaseDir := strings.Split(filepath.Dir(test.envPathToDb), \"/\")[0]\n\t\t\tif test.changePermission {\n\t\t\t\terr := os.Mkdir(baseDir, os.ModeDir)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"Can't create dir: %s\", baseDir)\n\t\t\t\t}\n\t\t\t\terr = os.Chmod(baseDir, 0)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"Can't change the mode dir in %s: %s\", baseDir, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tSetNewDbPathFromEnv()\n\t\t\tdefer os.RemoveAll(baseDir)\n\t\t\tdefer ChangeDbPath(dbPathOld)\n\n\t\t\tif test.expectedDBPath != DbPath {\n\t\t\t\tt.Errorf(\"[%s] Paths is not equals, expected: %s, got: %s\", test.name, test.expectedDBPath, DbPath)\n\t\t\t}\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "dbservice/dbservice_test.go",
    "content": "package dbservice\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.etcd.io/bbolt\"\n)\n\nvar (\n\tAlpineImageKey    = \"sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1-alpine:3.8-Docker Hub\"\n\tAlpineImageResult = `{\n\t\t\"image\": \"alpine:3.8\",\n\t\t\"registry\": \"Docker Hub\",\n\t\t\"digest\": \"sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1\",\n\t\t\"previous_digest\": \"sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1\",\n\t\t\"image_assurance_results\": {\n\t\t\t\"disallowed\": true,\n\t\t\t\"checks_performed\": [\n\t\t\t\t{\n\t\t\t\t\t\"control\": \"max_severity\",\n\t\t\t\t\t\"policy_name\": \"Default\",\n\t\t\t\t\t\"failed\": false\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"control\": \"trusted_base_images\",\n\t\t\t\t\t\"policy_name\": \"Default\",\n\t\t\t\t\t\"failed\": true\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"control\": \"max_score\",\n\t\t\t\t\t\"policy_name\": \"Default\",\n\t\t\t\t\t\"failed\": false\n\t\t\t\t}\n\t\t\t]\n\t\t},\n\t\t\"vulnerability_summary\": {\n\t\t\t\"total\": 2,\n\t\t\t\"critical\": 0,\n\t\t\t\"high\": 0,\n\t\t\t\"medium\": 2,\n\t\t\t\"low\": 0,\n\t\t\t\"negligible\": 0,\n\t\t\t\"sensitive\": 0,\n\t\t\t\"malware\": 0\n\t\t},\n\t\t\"scan_options\": {\n\t\t\t\"scan_sensitive_data\": true,\n\t\t\t\"scan_malware\": true\n\t\t},\n\t\t\"resources\": [\n\t\t\t{\n\t\t\t\t\"vulnerabilities\": [\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"CVE-2018-20679\",\n\t\t\t\t\t\t\"version\": \"\",\n\t\t\t\t\t\t\"fix_version\": \"\",\n\t\t\t\t\t\t\"aqua_severity\": \"medium\"\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t\"name\": \"CVE-2019-5747\",\n\t\t\t\t\t\t\"version\": \"\",\n\t\t\t\t\t\t\"fix_version\": \"\",\n\t\t\t\t\t\t\"aqua_severity\": \"medium\"\n\t\t\t\t\t}\n\t\t\t\t],\n\t\t\t\t\"resource\": {\n\t\t\t\t\t\"name\": \"busybox\",\n\t\t\t\t\t\"version\": \"1.28.4-r3\"\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}`\n)\n\nfunc TestStoreMessage(t *testing.T) {\n\tvar tests = []struct {\n\t\tinput *string\n\t}{\n\t\t{&AlpineImageResult},\n\t}\n\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\n\tfor _, test := range tests {\n\n\t\t// Handling of first scan\n\t\tisNew, err := MayBeStoreMessage([]byte(*test.input), AlpineImageKey, nil)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error: %s\\n\", err)\n\t\t}\n\t\tif !isNew {\n\t\t\tt.Errorf(\"A first scan was found!\\n\")\n\t\t}\n\n\t\t// Handling of second scan with the same data\n\t\tisNew, err = MayBeStoreMessage([]byte(*test.input), AlpineImageKey, nil)\n\t\tif err != nil {\n\t\t\tt.Errorf(\"Error: %s\\n\", err)\n\t\t}\n\t\tif isNew {\n\t\t\tt.Errorf(\"A old scan wasn't found!\\n\")\n\t\t}\n\t}\n\n}\nfunc TestInitError(t *testing.T) {\n\toriginalInit := Init\n\toriginalDbPath := DbPath\n\tinitErr := errors.New(\"init error\")\n\n\tDbPath = \"test_webhooks.db\"\n\n\tInit = func(db *bbolt.DB, bucket string) error {\n\t\treturn initErr\n\t}\n\n\tdefer func() {\n\t\tInit = originalInit\n\t\tos.Remove(DbPath)\n\t\tDbPath = originalDbPath\n\t}()\n\tisNew, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil)\n\n\tif isNew {\n\t\tt.Errorf(\"Scan shouldn't be marked as new\\n\")\n\t}\n\n\tif !errors.Is(err, initErr) {\n\t\tt.Errorf(\"Unexpected error: expected %s, got %s \\n\", initErr, err)\n\t}\n\n}\nfunc TestSelectError(t *testing.T) {\n\toriginalDbSelect := dbSelect\n\toriginalDbPath := DbPath\n\tselectErr := errors.New(\"select error\")\n\n\tDbPath = \"test_webhooks.db\"\n\n\tdbSelect = func(db *bbolt.DB, bucket, key string) (result []byte, err error) {\n\t\treturn nil, selectErr\n\t}\n\n\tdefer func() {\n\t\tdbSelect = originalDbSelect\n\t\tos.Remove(DbPath)\n\t\tDbPath = originalDbPath\n\t}()\n\tisNew, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil)\n\n\tif isNew {\n\t\tt.Errorf(\"Scan shouldn't be marked as new\\n\")\n\t}\n\n\tif !errors.Is(err, selectErr) {\n\t\tt.Errorf(\"Unexpected error: expected %s, got %s \\n\", selectErr, err)\n\t}\n\n}\nfunc TestInsertError(t *testing.T) {\n\tvar tests = []struct {\n\t\tbucket string\n\t}{\n\t\t{\"WebhookBucket\"},\n\t\t{\"WebookExpiryDates\"},\n\t}\n\tfor _, test := range tests {\n\t\ttestBucketInsert(t, test.bucket)\n\t}\n}\n\nfunc testBucketInsert(t *testing.T, testBucket string) {\n\toriginalDbInsert := dbInsert\n\toriginalDbPath := DbPath\n\tinsertErr := errors.New(\"insert error\")\n\n\tDbPath = \"test_webhooks.db\"\n\n\tdbInsert = func(db *bbolt.DB, bucket string, key, value []byte) error {\n\t\tif bucket == testBucket {\n\t\t\treturn insertErr\n\t\t}\n\t\treturn nil\n\t}\n\n\tdefer func() {\n\t\tdbInsert = originalDbInsert\n\t\tos.Remove(DbPath)\n\t\tDbPath = originalDbPath\n\t}()\n\t//expired shouldn't be null to cause insert to 'WebookExpiryDates' bucket\n\ttimeToExpire := time.Duration(1) * time.Second\n\texpired := time.Now().UTC().Add(timeToExpire)\n\n\tisNew, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, &expired)\n\n\tif isNew {\n\t\tt.Errorf(\"Scan shouldn't be marked as new\\n\")\n\t}\n\n\tif !errors.Is(err, insertErr) {\n\t\tt.Errorf(\"Unexpected error: expected %s, got %s \\n\", insertErr, err)\n\t}\n}\n"
  },
  {
    "path": "dbservice/delete.go",
    "content": "package dbservice\n\nimport bolt \"go.etcd.io/bbolt\"\n\nfunc dbDelete(db *bolt.DB, bucket string, keys [][]byte) error {\n\treturn db.Update(func(tx *bolt.Tx) error {\n\t\tb := tx.Bucket([]byte(bucket))\n\t\tif b == nil {\n\t\t\treturn nil\n\t\t}\n\t\tfor _, key := range keys {\n\t\t\tif err := b.Delete(key); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "dbservice/init.go",
    "content": "package dbservice\n\nimport \"go.etcd.io/bbolt\"\n\nvar Init = func(db *bbolt.DB, bucket string) error {\n\treturn db.Update(func(tx *bbolt.Tx) error {\n\t\t_, err := tx.CreateBucketIfNotExists([]byte(bucket))\n\t\treturn err\n\t})\n}\n"
  },
  {
    "path": "dbservice/insert.go",
    "content": "package dbservice\n\nimport bolt \"go.etcd.io/bbolt\"\n\nvar dbInsert = func(db *bolt.DB, bucket string, key, value []byte) error {\n\terr := db.Update(func(tx *bolt.Tx) error {\n\t\tb, err := tx.CreateBucketIfNotExists([]byte(bucket))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn b.Put(key, value)\n\t})\n\treturn err\n}\n"
  },
  {
    "path": "dbservice/invalidinit_test.go",
    "content": "package dbservice\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"testing\"\n\n\t\"go.etcd.io/bbolt\"\n)\n\nvar tests = []struct {\n\tcaseDesc        string\n\terrPrvdr        func() error\n\tinitIsNotCalled bool\n}{\n\t{\n\t\tcaseDesc: \"EnsureApiKey\",\n\t\terrPrvdr: func() error {\n\t\t\treturn EnsureApiKey()\n\t\t},\n\t},\n\t{\n\t\tcaseDesc: \"GetApiKey\",\n\t\terrPrvdr: func() error {\n\t\t\t_, err := GetApiKey()\n\t\t\treturn err\n\t\t},\n\t\tinitIsNotCalled: true,\n\t},\n\t{\n\t\tcaseDesc: \"RegisterPlgnInvctn\",\n\t\terrPrvdr: func() error {\n\t\t\treturn RegisterPlgnInvctn(\"some-key\")\n\t\t},\n\t},\n\t{\n\t\tcaseDesc: \"MayBeStoreMessage\",\n\t\terrPrvdr: func() error {\n\t\t\t_, err := MayBeStoreMessage(nil, \"a-b-c\", nil)\n\t\t\treturn err\n\t\t},\n\t},\n\t{\n\t\tcaseDesc: \"AggregateScans\",\n\t\terrPrvdr: func() error {\n\t\t\t_, err := AggregateScans(\"\", map[string]string{}, 1, false)\n\t\t\treturn err\n\t\t},\n\t},\n}\n\nfunc TestInvalidDbPath(t *testing.T) {\n\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"/tmp\"\n\n\tfor _, test := range tests {\n\t\terr := test.errPrvdr()\n\t\tif err == nil {\n\t\t\tt.Errorf(\"Error is expected when %s is called\\n\", test.caseDesc)\n\t\t}\n\n\t}\n}\nfunc TestBucketInitialization(t *testing.T) {\n\tsavedInit := Init\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tInit = savedInit\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\texpectedError := errors.New(\"weird error\")\n\tInit = func(db *bbolt.DB, bucket string) error {\n\t\treturn expectedError\n\t}\n\tfor _, test := range tests {\n\t\tif test.initIsNotCalled {\n\t\t\tcontinue\n\t\t}\n\t\terr := test.errPrvdr()\n\t\tif !errors.Is(err, expectedError) {\n\t\t\tt.Errorf(\"Unexpected error for %s call, expected %v, got %v\", test.caseDesc, expectedError, err)\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "dbservice/plgnstats.go",
    "content": "package dbservice\n\nimport (\n\t\"strconv\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc RegisterPlgnInvctn(name string) error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\n\tdb, err := bolt.Open(DbPath, 0666, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer db.Close()\n\terr = Init(db, DbBucketActionStats)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = db.Update(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket([]byte(DbBucketActionStats))\n\t\tvar i int\n\t\tv := bucket.Get([]byte(name))\n\n\t\tif v == nil {\n\t\t\ti = 0\n\t\t} else {\n\t\t\ti, err = strconv.Atoi(string(v[:]))\n\t\t}\n\n\t\ti++\n\t\tnwv := strconv.Itoa(i)\n\n\t\terr = bucket.Put([]byte(name), []byte(nwv))\n\t\treturn err\n\t})\n\n\treturn err\n}\n"
  },
  {
    "path": "dbservice/plgnstats_test.go",
    "content": "package dbservice\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc TestRegisterPlgnInvctn(t *testing.T) {\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\texpectedCnt := 3\n\tkeyToTest := \"test\"\n\tfor i := 0; i < expectedCnt; i++ {\n\t\terr := RegisterPlgnInvctn(keyToTest)\n\t\tif err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t}\n\tr, err := getPlgnStats()\n\tif err != nil {\n\t\tt.Fatal(\"error while getting value of API key\")\n\t}\n\tif r[keyToTest] != expectedCnt {\n\t\tt.Errorf(\"Persisted count doesn't match expected. Expected %d, got %d\\n\", r[keyToTest], expectedCnt)\n\t}\n\n}\n\nfunc getPlgnStats() (r map[string]int, err error) {\n\tr = make(map[string]int)\n\n\tdb, err := bolt.Open(DbPath, 0444, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer db.Close()\n\terr = db.View(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket([]byte(DbBucketActionStats))\n\t\tif bucket == nil {\n\t\t\treturn nil //no bucket - empty stats will be returned\n\t\t}\n\n\t\tc := bucket.Cursor()\n\n\t\tfor k, v := c.First(); k != nil; k, v = c.Next() {\n\t\t\tcnt, err := strconv.Atoi(string(v[:]))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tr[string(k[:])] = cnt\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn r, nil\n}\n"
  },
  {
    "path": "dbservice/select.go",
    "content": "package dbservice\n\nimport (\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nvar dbSelect = func(db *bolt.DB, bucket, key string) (result []byte, err error) {\n\terr = db.View(func(tx *bolt.Tx) error {\n\t\tb := tx.Bucket([]byte(bucket))\n\t\tr := b.Get([]byte(key))\n\t\tif r != nil {\n\t\t\tresult = make([]byte, len(r))\n\t\t\tcopy(result, r)\n\t\t}\n\t\treturn nil\n\t})\n\treturn\n}\n"
  },
  {
    "path": "dbservice/sharedcfg.go",
    "content": "package dbservice\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nconst (\n\tapiKeyName = \"POSTEE_API_KEY\"\n)\n\nfunc getDbPath() string {\n\tvar dbPath string\n\tif len(os.Getenv(\"PATH_TO_DB\")) > 0 {\n\t\tdbPath = os.Getenv(\"PATH_TO_DB\")\n\t} else {\n\t\tdbPath = DbPath\n\t}\n\treturn dbPath\n}\n\nfunc EnsureApiKey() error {\n\tmutex.Lock()\n\tdefer mutex.Unlock()\n\n\tdb, err := bolt.Open(getDbPath(), 0666, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer db.Close()\n\n\terr = Init(db, DbBucketActionStats)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnewApiKey, err := generateApiKey(32)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = dbInsert(db, DbBucketSharedConfig, []byte(apiKeyName), []byte(newApiKey))\n\n\treturn err\n}\nfunc GetApiKey() (string, error) {\n\tvar apiKey string = \"\"\n\tdb, err := bolt.Open(getDbPath(), 0444, nil) //should be enough\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer db.Close()\n\terr = db.View(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket([]byte(DbBucketSharedConfig))\n\t\tif bucket == nil {\n\t\t\treturn errors.New(\"no bucket\") //no bucket\n\t\t}\n\n\t\tbytes := bucket.Get([]byte(apiKeyName))\n\n\t\tapiKey = string(bytes[:])\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn apiKey, nil\n\n}\nfunc generateApiKey(length int) (string, error) {\n\tk := make([]byte, length)\n\tif _, err := io.ReadFull(rand.Reader, k); err != nil {\n\t\treturn \"\", err\n\t}\n\treturn hex.EncodeToString(k), nil\n}\n"
  },
  {
    "path": "dbservice/sharedcfg_test.go",
    "content": "package dbservice\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestApiKey(t *testing.T) {\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\terr := EnsureApiKey()\n\tif err != nil {\n\t\tt.Fatal(\"error EnsureApiKey\")\n\t}\n\tkey, err := GetApiKey()\n\tif err != nil {\n\t\tt.Fatal(\"error while getting value of API key\")\n\t}\n\tif key == \"\" {\n\t\tt.Fatal(\"empty key received\")\n\t}\n}\nfunc TestApiKeyWithoutInit(t *testing.T) {\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\tkey, err := GetApiKey()\n\tif err == nil {\n\t\tt.Fatal(\"Error is expected\")\n\t}\n\tif key != \"\" {\n\t\tt.Fatal(\"Empty key is expected\")\n\t}\n}\nfunc TestApiKeyRenewal(t *testing.T) {\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\tos.Remove(DbPath)\n\t\tDbPath = dbPathReal\n\t}()\n\tDbPath = \"test_webhooks.db\"\n\tvar keys [2]string\n\tfor i := 0; i < 2; i++ {\n\t\terr := EnsureApiKey()\n\t\tif err != nil {\n\t\t\tt.Errorf(\"error EnsureApiKey: %s\", err)\n\t\t}\n\t\tkey, err := GetApiKey()\n\t\tif err != nil {\n\t\t\tt.Fatal(\"error while getting value of API key\")\n\t\t}\n\t\tif key == \"\" {\n\t\t\tt.Fatal(\"empty key received\")\n\t\t}\n\t\tkeys[i] = key\n\n\t}\n\tif keys[0] == keys[1] {\n\t\tt.Errorf(\"Key is not updated. (before: %s and after update: %s)\", keys[0], keys[1])\n\t}\n}\n"
  },
  {
    "path": "deploy/helm/postee/.helmignore",
    "content": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation (prefixed with !). Only one pattern per line.\n.DS_Store\n# Common VCS dirs\n.git/\n.gitignore\n.bzr/\n.bzrignore\n.hg/\n.hgignore\n.svn/\n# Common backup files\n*.swp\n*.bak\n*.tmp\n*.orig\n*~\n# Various IDEs\n.project\n.idea/\n*.tmproj\n.vscode/\n"
  },
  {
    "path": "deploy/helm/postee/Chart.yaml",
    "content": "apiVersion: v2\nname: postee\ndescription: A Helm chart for Postee\ntype: application\n\n# Versions are expected to follow Semantic Versioning (https://semver.org/)\nversion: 0.5.0\n\n# This is the version number of the application being deployed. This version number should be\n# incremented each time you make changes to the application. Versions are not expected to\n# follow Semantic Versioning. They should reflect the version the application is using.\n# It is recommended to use it with quotes.\nappVersion: \"2.14.0-amd64\"\n\nkeywords:\n  - aquasecurity\n  - postee\nsources:\n  - https://github.com/aquasecurity/postee\n"
  },
  {
    "path": "deploy/helm/postee/templates/NOTES.txt",
    "content": "1. Get the application URL by running these commands:\n{{- if .Values.ingress.enabled }}\n{{- range $host := .Values.ingress.hosts }}\n  {{- range .paths }}\n  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }}\n  {{- end }}\n{{- end }}\n{{- else if contains \"NodePort\" .Values.service.type }}\n  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath=\"{.spec.ports[0].nodePort}\" services {{ include \"postee.fullname\" . }})\n  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath=\"{.items[0].status.addresses[0].address}\")\n  echo http://$NODE_IP:$NODE_PORT\n{{- else if contains \"LoadBalancer\" .Values.service.type }}\n     NOTE: It may take a few minutes for the LoadBalancer IP to be available.\n           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include \"postee.fullname\" . }}'\n  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include \"postee.fullname\" . }} --template \"{{\"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}\"}}\")\n  echo http://$SERVICE_IP:{{ .Values.service.port }}\n{{- else if contains \"ClusterIP\" .Values.service.type }}\n  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l \"app.kubernetes.io/name={{ include \"postee.name\" . }},app.kubernetes.io/instance={{ .Release.Name }}\" -o jsonpath=\"{.items[0].metadata.name}\")\n  export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath=\"{.spec.containers[0].ports[0].containerPort}\")\n  echo \"Postee Webhook http://{{ include \"postee.fullname\" . }}:{{ .Values.service.port }} to set in your application.\"\n  echo \"Postee Website http://{{ include \"postee.ui.fullname\" . }}:{{ .Values.uiService.port }} to configure Postee.\"\n  kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME {{ .Values.uiService.port }}:$CONTAINER_PORT\n{{- end }}\n"
  },
  {
    "path": "deploy/helm/postee/templates/_helpers.tpl",
    "content": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"postee.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCreate a default fully qualified app name.\nWe truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).\nIf release name contains chart name it will be used as a full name.\n*/}}\n{{- define \"postee.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}\n{{- else }}\n{{- printf \"%s-%s\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{- define \"postee.ui.fullname\" -}}\n{{- if .Values.fullnameOverride }}\n{{- .Values.fullnameOverride | trunc 63 | trimSuffix \"-\" }}ui\n{{- else }}\n{{- $name := default .Chart.Name .Values.nameOverride }}\n{{- if contains $name .Release.Name }}\n{{- .Release.Name | trunc 63 | trimSuffix \"-\" }}ui\n{{- else }}\n{{- printf \"%s-%sui\" .Release.Name $name | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n{{- end }}\n{{- end }}\n\n{{/*\nCreate chart name and version as used by the chart label.\n*/}}\n{{- define \"postee.chart\" -}}\n{{- printf \"%s-%s\" .Chart.Name .Chart.Version | replace \"+\" \"_\" | trunc 63 | trimSuffix \"-\" }}\n{{- end }}\n\n{{/*\nCommon labels\n*/}}\n{{- define \"postee.labels\" -}}\nhelm.sh/chart: {{ include \"postee.chart\" . }}\n{{ include \"postee.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{- define \"postee.ui.labels\" -}}\nhelm.sh/chart: {{ include \"postee.chart\" . }}\n{{ include \"postee.ui.selectorLabels\" . }}\n{{- if .Chart.AppVersion }}\napp.kubernetes.io/version: {{ .Chart.AppVersion | quote }}\n{{- end }}\napp.kubernetes.io/managed-by: {{ .Release.Service }}\n{{- end }}\n\n{{/*\nSelector labels\n*/}}\n{{- define \"postee.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"postee.name\" . }}\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{- define \"postee.ui.selectorLabels\" -}}\napp.kubernetes.io/name: {{ include \"postee.name\" . }}-ui\napp.kubernetes.io/instance: {{ .Release.Name }}\n{{- end }}\n\n{{/*\nCreate the name of the service account to use\n*/}}\n{{- define \"postee.serviceAccountName\" -}}\n{{- if .Values.serviceAccount.create }}\n{{- default (include \"postee.fullname\" .) .Values.serviceAccount.name }}\n{{- else }}\n{{- default \"default\" .Values.serviceAccount.name }}\n{{- end }}\n{{- end }}\n"
  },
  {
    "path": "deploy/helm/postee/templates/cfg-secret.yaml",
    "content": "{{ if not .Values.configuration.existingSecret.enabled }}\napiVersion: v1\nkind: Secret\nmetadata:\n  name: {{ include \"postee.fullname\" . }}-secret\ndata:\n  cfg.yaml: |\n{{ .Values.posteeConfig  | b64enc | indent 4 }}\n{{- end }}\n"
  },
  {
    "path": "deploy/helm/postee/templates/ingress.yaml",
    "content": "{{- if .Values.ingress.enabled -}}\n{{- $fullName := include \"postee.fullname\" . -}}\n{{- $svcPort := .Values.service.port -}}\n{{- if semverCompare \">=1.14-0 < 1.19.0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1beta1\n{{- else if semverCompare \">=1.19-0\" .Capabilities.KubeVersion.GitVersion -}}\napiVersion: networking.k8s.io/v1\n{{- else -}}\napiVersion: extensions/v1beta1\n{{- end }}\nkind: Ingress\nmetadata:\n  name: {{ $fullName }}\n  labels:\n    {{- include \"postee.labels\" . | nindent 4 }}\n  {{- with .Values.ingress.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\nspec:\n  {{- if .Values.ingress.tls }}\n  tls:\n    {{- range .Values.ingress.tls }}\n    - hosts:\n        {{- range .hosts }}\n        - {{ . | quote }}\n        {{- end }}\n      secretName: {{ .secretName }}\n    {{- end }}\n  {{- end }}\n  rules:\n    {{- if semverCompare \">=1.19-0\" .Capabilities.KubeVersion.GitVersion -}}\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            pathType: {{ .pathType }}\n            backend:\n              service:\n                name: {{ $fullName }}\n                port:\n                  number: {{ $svcPort }}\n          {{- end }}\n    {{- end }}\n    {{- else -}}\n    {{- range .Values.ingress.hosts }}\n    - host: {{ .host | quote }}\n      http:\n        paths:\n          {{- range .paths }}\n          - path: {{ .path }}\n            backend:\n              serviceName: {{ $fullName }}\n              servicePort: {{ $svcPort }}\n          {{- end }}\n    {{- end }}\n    {{- end }}\n  {{- end }}\n"
  },
  {
    "path": "deploy/helm/postee/templates/postee-svc.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"postee.fullname\" . }}\n  labels:\n    {{- include \"postee.labels\" . | nindent 4 }}\nspec:\n  type: {{ .Values.service.type }}\n  ports:\n    - port: {{ .Values.service.port }}\n      targetPort: {{ .Values.service.targetPort }}\n      protocol: TCP\n      name: http\n    - port: {{ .Values.service.portSsl }}\n      targetPort: {{ .Values.service.targetPortSsl }}\n      protocol: TCP\n      name: http-ssl\n  selector:\n    {{- include \"postee.selectorLabels\" . | nindent 4 }}\n"
  },
  {
    "path": "deploy/helm/postee/templates/postee-ui-secret.yaml",
    "content": "{{- if not .Values.posteUi.existingSecret.enabled }}\napiVersion: v1\nkind: Secret\nmetadata:\n  name: {{ include \"postee.ui.fullname\" . }}-creds\n  labels:\n    {{- include \"postee.ui.labels\" . | nindent 4 }}\ntype: Opaque\ndata:\n  postee-ui-user: {{ .Values.posteUi.user | b64enc | quote }}\n  postee-ui-password: {{ .Values.posteUi.pass | b64enc | quote }}\n{{- end }}"
  },
  {
    "path": "deploy/helm/postee/templates/postee-ui-svc.yaml",
    "content": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"postee.ui.fullname\" . }}\n  labels:\n    {{- include \"postee.ui.labels\" . | nindent 4 }}\nspec:\n  type: {{ .Values.uiService.type }}\n  ports:\n    - port: {{ .Values.uiService.port }}\n      targetPort: {{ .Values.uiService.targetPort }}\n      protocol: TCP\n      name: http\n  selector:\n    {{- include \"postee.ui.selectorLabels\" . | nindent 4 }}\n"
  },
  {
    "path": "deploy/helm/postee/templates/postee-ui.yaml",
    "content": "{{- $fullName := include \"postee.fullname\" . -}}\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"postee.ui.fullname\" . }}\n  labels:\n    {{- include \"postee.ui.labels\" . | nindent 4 }}\nspec:\n  {{- if not .Values.autoscaling.enabled }}\n  replicas: {{ .Values.replicaCount }}\n  {{- end }}\n  selector:\n    matchLabels:\n      {{- include \"postee.ui.selectorLabels\" . | nindent 6 }}\n  template:\n    metadata:\n      {{- with .Values.podAnnotations }}\n      annotations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      labels:\n        {{- include \"postee.ui.selectorLabels\" . | nindent 8 }}\n    spec:\n      {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      serviceAccountName: {{ include \"postee.serviceAccountName\" . }}\n      securityContext:\n        {{- toYaml .Values.podSecurityContext | nindent 8 }}\n      initContainers:\n        - name: setting-cfg\n          image: \"{{ .Values.imageInit.repository }}:{{ .Values.imageInit.tag }}\"\n          imagePullPolicy: {{ .Values.imageInit.pullPolicy }}\n          command: [\"/bin/chown\", \"-R\", \"1099\", \"{{ .Values.persistentVolume.mountPathConfig }}\"]\n          volumeMounts:\n            - name: postee-config\n              mountPath: {{ .Values.persistentVolume.mountPathConfig }}\n      containers:\n        - name: {{ .Chart.Name }}\n          securityContext:\n            {{- toYaml .Values.securityContext | nindent 12 }}\n          image: \"{{ .Values.posteUi.image }}:{{ .Values.posteUi.tag | default .Chart.AppVersion }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          env:\n            - name: POSTEE_UI_CFG\n              value: {{ .Values.persistentVolume.mountPathConfig }}/cfg.yaml\n            - name: POSTEE_UI_PORT\n              value: {{ .Values.posteUi.port | quote }}\n            - name: POSTEE_UI_UPDATE_URL\n              value: \"http://{{ include \"postee.fullname\" . }}:{{ .Values.service.port }}\"\n            - name: POSTEE_ADMIN_USER\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Values.posteUi.existingSecret.secretName | default (printf \"%s-creds\" (include \"postee.ui.fullname\" .)) }}\n                  key: {{ .Values.posteUi.existingSecret.usernameKey | default \"postee-ui-user\" }}\n            - name: POSTEE_ADMIN_PASSWORD\n              valueFrom:\n                secretKeyRef:\n                  name: {{ .Values.posteUi.existingSecret.secretName | default (printf \"%s-creds\" (include \"postee.ui.fullname\" .)) }}\n                  key: {{ .Values.posteUi.existingSecret.passwordKey | default \"postee-ui-password\" }}\n          ports:\n            - name: http\n              containerPort: {{ .Values.posteUi.port }}\n              protocol: TCP\n          volumeMounts:\n            - name: postee-config\n              mountPath: {{ .Values.persistentVolume.mountPathConfig }}\n            - name: postee-db\n              mountPath: {{ .Values.persistentVolume.mountPathDb }}\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n      volumes:\n        - name: postee-db\n          persistentVolumeClaim:\n            claimName: \"{{ $fullName }}-db-{{ $fullName }}-0\"\n        - name: postee-config\n          persistentVolumeClaim:\n            claimName: \"{{ $fullName }}-config-{{ $fullName }}-0\"\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}"
  },
  {
    "path": "deploy/helm/postee/templates/postee.yaml",
    "content": "{{- $fullName := include \"postee.fullname\" . -}}\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  name: {{ include \"postee.fullname\" . }}\n  labels:\n    {{- include \"postee.labels\" . | nindent 4 }}\nspec:\n  {{- if not .Values.autoscaling.enabled }}\n  replicas: {{ .Values.replicaCount }}\n  {{- end }}\n  selector:\n    matchLabels:\n      {{- include \"postee.selectorLabels\" . | nindent 6 }}\n  serviceName: {{ include \"postee.fullname\" . }}\n  volumeClaimTemplates:\n    - metadata:\n        name: {{ $fullName }}-db\n      spec:\n        accessModes: [ \"ReadWriteOnce\" ]\n        resources:\n          requests:\n            storage: 1Gi\n    - metadata:\n        name: {{ $fullName }}-config\n      spec:\n        accessModes: [ \"ReadWriteOnce\" ]\n        resources:\n          requests:\n            storage: 100Mi\n    - metadata:\n        name: {{ $fullName }}-rego-template\n      spec:\n        accessModes: [ \"ReadWriteOnce\" ]\n        resources:\n          requests:\n            storage: 100Mi\n    - metadata:\n        name: {{ $fullName }}-filters\n      spec:\n        accessModes: [ \"ReadWriteOnce\" ]\n        resources:\n          requests:\n            storage: 100Mi\n  template:\n    metadata:\n      annotations:\n      {{- if not .Values.configuration.existingSecret.enabled }}\n        checksum/secret: {{ include (print $.Template.BasePath \"/cfg-secret.yaml\") . | sha256sum }}\n      {{- end }}\n      {{- with .Values.podAnnotations }}\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      labels:\n        {{- include \"postee.selectorLabels\" . | nindent 8 }}\n    spec:\n      {{- with .Values.imagePullSecrets }}\n      imagePullSecrets:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      serviceAccountName: {{ include \"postee.serviceAccountName\" . }}\n      securityContext:\n        {{- toYaml .Values.podSecurityContext | nindent 8 }}\n      initContainers:\n        - name: setting-db\n          image: \"{{ .Values.imageInit.repository }}:{{ .Values.imageInit.tag }}\"\n          imagePullPolicy: {{ .Values.imageInit.pullPolicy }}\n          command: [\"/bin/chown\", \"-R\", \"1099\", \"{{ .Values.persistentVolume.mountPathDb }}\"]\n          volumeMounts:\n            - name: {{ $fullName }}-db\n              mountPath: {{ .Values.persistentVolume.mountPathDb }}\n        - name: setting-cfg\n          image: \"{{ .Values.imageInit.repository }}:{{ .Values.imageInit.tag }}\"\n          imagePullPolicy: {{ .Values.imageInit.pullPolicy }}\n          command: [\"/bin/cp\", \"/k8s/cfg.yaml\", \"/data/cfg.yaml\"]\n          volumeMounts:\n            - name: {{ $fullName }}-secret-vol\n              mountPath: /k8s\n            - name: {{ $fullName }}-config\n              mountPath: {{ .Values.persistentVolume.mountPathConfig }}\n      containers:\n        - name: {{ .Chart.Name }}\n          securityContext:\n            {{- toYaml .Values.securityContext | nindent 12 }}\n          image: \"{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}\"\n          imagePullPolicy: {{ .Values.image.pullPolicy }}\n          env:\n            - name: POSTEE_CFG\n              value: /data/cfg.yaml\n            - name: POSTEE_DEBUG\n              value: \"not\"\n          {{- with .Values.envFrom }}\n          envFrom:\n            {{- range . }}\n            - secretRef:\n                name: {{ . }}\n            {{- end }}\n          {{- end }}\n          ports:\n            - name: http\n              containerPort: {{ .Values.service.targetPort }}\n              protocol: TCP\n            - name: tls\n              containerPort: {{ .Values.service.targetPortSsl }}\n              protocol: TCP\n          volumeMounts:\n            - name: {{ $fullName }}-db\n              mountPath: {{ .Values.persistentVolume.mountPathDb }}\n            - name: {{ $fullName }}-config\n              mountPath: {{ .Values.persistentVolume.mountPathConfig }}\n            - name: {{ $fullName }}-rego-template\n              mountPath: {{ .Values.persistentVolume.mountPathRego }}\n            - name: {{ $fullName }}-filters\n              mountPath: {{ .Values.persistentVolume.mountPathFilters }}\n{{- if .Values.extraVolumeMounts }}\n{{ toYaml .Values.extraVolumeMounts | indent 12 }}\n{{- end }}\n          resources:\n            {{- toYaml .Values.resources | nindent 12 }}\n      volumes:\n        - secret:\n            secretName: {{ $fullName }}-secret\n          name: {{ $fullName }}-secret-vol\n{{- if .Values.extraVolumes }}\n{{ toYaml .Values.extraVolumes | indent 8 }}\n{{- end }}\n      {{- with .Values.nodeSelector }}\n      nodeSelector:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.affinity }}\n      affinity:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n      {{- with .Values.tolerations }}\n      tolerations:\n        {{- toYaml . | nindent 8 }}\n      {{- end }}\n"
  },
  {
    "path": "deploy/helm/postee/templates/pvc.yaml",
    "content": "{{- if and .Values.persistentVolume.enabled (not .Values.persistentVolume.existingClaim) }}\n{{- $fullName := include \"postee.fullname\" . -}}\nkind: PersistentVolumeClaim\napiVersion: v1\nmetadata:\n  labels:\n    {{- include \"postee.labels\" . | nindent 4 }}\n  name: {{ include \"postee.fullname\" . }}-pvc\nspec:\n  accessModes:\n  {{- range .Values.persistentVolume.accessModes }}\n    - {{ . | quote }}\n  {{- end }}\n  resources:\n    requests:\n      storage: {{ .Values.persistentVolume.size | quote }}\n{{- end }}"
  },
  {
    "path": "deploy/helm/postee/templates/serviceaccount.yaml",
    "content": "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n  name: {{ include \"postee.serviceAccountName\" . }}\n  labels:\n    {{- include \"postee.labels\" . | nindent 4 }}\n  {{- with .Values.serviceAccount.annotations }}\n  annotations:\n    {{- toYaml . | nindent 4 }}\n  {{- end }}\n{{- end }}\n"
  },
  {
    "path": "deploy/helm/postee/templates/tests/test-connection.yaml",
    "content": "apiVersion: v1\nkind: Pod\nmetadata:\n  name: \"{{ include \"postee.fullname\" . }}-test-connection\"\n  labels:\n    {{- include \"postee.labels\" . | nindent 4 }}\n  annotations:\n    \"helm.sh/hook\": test\nspec:\n  containers:\n    - name: wget\n      image: busybox\n      command: ['wget']\n      args: ['{{ include \"postee.fullname\" . }}:{{ .Values.service.Port }}']\n  restartPolicy: Never\n"
  },
  {
    "path": "deploy/helm/postee/values.yaml",
    "content": "# Default values for postee.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nreplicaCount: 1\nposteeConfig: |\n  # The configuration file contains a general settings section,\n  # routes, templates and actions sections.\n\n  name: tenant            #  The tenant name\n  aqua-server:            #  URL of Aqua Server for links. E.g. https://myserver.aquasec.com\n  max-db-size: 1000MB       #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\n  db-verify-interval: 1   #  How often to check the DB size. By default, Postee checks every 1 hour\n\n  # Routes are used to define how to handle an incoming message\n  routes:\n  - name: stdout\n    actions: [ stdout ]\n    template: raw-json\n\n  #- name: route1                                 #  Route name. Must be unique\n  #  input: contains(input.image, \"alpine\")       #  REGO rule to match input message against route\n  #  input-files:                                  #  Array filePaths to files with REGO rules\n  #   - Allow-Image-Name.rego\n  #   - Ignore-Image-Name.rego\n  #   - Allow-Registry.rego\n  #   - Ignore-Registry.rego\n  #   - Policy-Only-Fix-Available.rego\n  #   - Policy-Min-Vulnerability.rego\n  #   - Policy-Related-Features.rego\n  #  actions: [my-slack]                          #  Action name (needs to be defined under \"actions\") which will receive the message\n  #  template: slack-template                     #  Template name (needs to be defined under \"templates\") which will be used to process the message output format\n  #  plugins:                                     #  Optional plugins\n  #   aggregate-message-number:                   # Number of same messages to aggregate into one output message\n  #   aggregate-message-timeout:                  # Number of seconds/minutes/hours to aggregate same messages into one output. Maximum is 24 hours. Use Xs or Xm or Xh\n  #   unique-message-props: [\"digest\",\"image\",\"registry\", \"vulnerability_summary.high\", \"vulnerability_summary.medium\", \"vulnerability_summary.low\"] # Optional: Comma separated list of top level properties which uniqult identifies an event message. If message with same property values is received more than once it will be ignored\n  #   unique-message-timeout:                     # Number of seconds/minutes/hours/days before expiring of a message. Expired messages are removed from db. If option is empty message is never deleted\n\n  # - name: Trivy Operator Alerts\n  #   input: input.report.summary.criticalCount > 0 # You can customize this based on your needs\n  #   actions: [my-slack]\n  #   template: trivy-operator-slack\n\n  # - name: Trivy Operator Sbom Report to Dependency Track\n  #   input: contains(input.kind, \"SbomReport\")\n  #   actions: [ my-dependencytrack ]\n  #   template: trivy-operator-dependencytrack\n\n  # Templates are used to format a message\n  templates:\n  - name: vuls-slack                  #  Out of the box template for slack\n    rego-package:  postee.vuls.slack      #  Slack template REGO package (available out of the box)\n  - name: vuls-html                       #  Out of the box HTML template\n    rego-package:  postee.vuls.html       #  HTML template REGO package (available out of the box)\n  - name: raw-html                        #  Raw message json\n    rego-package:  postee.rawmessage.html #  HTML template REGO package (available out of the box)\n  - name: legacy                          #  Out of the box legacy Golang template\n    legacy-scan-renderer: html\n  - name: legacy-slack                    #  Legacy slack template implemented in Golang\n    legacy-scan-renderer: slack\n  - name: legacy-jira                     #  Legacy jira template implemented in Golang\n    legacy-scan-renderer: jira\n  - name: custom-email                    #  Example of how to use a template from a Web URL\n    url:                                  #  URL to custom REGO file\n  - name: raw-json                        # route message \"As Is\" to external webhook\n    rego-package: postee.rawmessage.json\n  - name: vuls-cyclonedx                  # export vulnerabilities to CycloneDX XML\n    rego-package: postee.vuls.cyclondx\n  - name: trivy-operator-jira\n    rego-package: postee.trivyoperator.jira\n  - name: trivy-operator-slack\n    rego-package: postee.trivyoperator.slack\n  - name: trivy-operator-dependencytrack\n    rego-package: postee.trivyoperator.dependencytrack\n\n  # Rules are predefined rego policies that can be used to trigger routes\n  rules:\n  - name: Initial Access\n  - name: Credential Access\n  - name: Privilege Escalation\n  - name: Defense Evasion\n  - name: Persistence\n\n  # Actions are target services that should consume the messages\n  actions:\n  - name: stdout\n    type: stdout\n    enable: true\n\n  - name: my-jira   #  name must be unique\n    type: jira      #  supported types: jira, email\n    enable: false\n    url:            # Mandatory. E.g \"https://johndoe.atlassian.net\"\n    user:           # Mandatory. E.g :johndoe@gmail.com\"\n    password:       # Optional. Specify Jira user API key. Used only for Jira Cloud\n    token:          # Optional. Specify Jira user Personal Access Token. Used only for Jira Server/Data Center\n    project-key:    # Mandatory. Specify the JIRA product key\n    tls-verify: false\n    board:          # Optional. Specify the Jira board name to open tickets on\n    labels:         # Optional, specify array of labels to add to Ticket, for example: [\"label1\", \"label2\"]\n    issuetype:      # Optional. Specifty the issue type to open (Bug, Task, etc.). Default is \"Task\"\n    priority:       # Optional. Specify the issues severity. Default is \"High\"\n    assignee:       # Optional. Specify the assigned user. Default is the user that opened the ticket\n\n  - name: my-email\n    type: email\n    enable: false\n    user:      # Optional (if auth supported): SMTP user name (e.g. johndoe@gmail.com)\n    password:  # Optional (if auth supported): SMTP password\n    host:      # Mandatory: SMTP host name (e.g. smtp.gmail.com)\n    port:      # Mandatory: SMTP server port (e.g. 587)\n    sender:    # Mandatory: The email address to use as a sender\n    recipients: [\"\", \"\"]  # Mandatory: comma separated list of recipients\n\n  - name: my-email-smtp-server\n    type: email\n    enable: false\n    use-mx: true\n    sender:  # Mandatory: The email address to use as a sender\n    recipients: [\"\", \"\"]  # Mandatory: comma separated list of recipients\n\n  - name: my-slack\n    type: slack\n    enable: false\n    url: https://hooks.slack.com/services/TAAAA/BBB/<key>\n\n  - name: ms-team\n    type: teams\n    enable: false\n    url: https://outlook.office.com/webhook/....   #  Webhook's url\n\n  - name: webhook\n    type: webhook\n    enable: false\n    url: https://..../webhook/   #  Webhook's url\n    timeout:                     #  Webhook's timeout. <numbers><unit suffix> pattern is used, such as \"300ms\" or \"2h45m\". Default: 120s\n\n  - name: splunk\n    type: splunk\n    enable: false\n    url: http://localhost:8088 # Mandatory. Url of a Splunk server\n    token: <token>             # Mandatory. a HTTP Event Collector Token\n    size-limit: 10000          # Optional. Maximum scan length, in bytes. Default: 10000\n    tls-verify: false          # Enable skip TLS Verification. Default: false.\n\n  - name: my-servicenow\n    type: serviceNow\n    enable: false\n    user:      # Mandatory. E.g :johndoe@gmail.com\"\n    password:  # Mandatory. Specify user API key\n    instance:  # Mandatory. Name of ServiceN  ow Instance\n    board:     #  Specify the ServiceNow board name to open tickets on. Default is \"incident\"\n\n  - name: my-nexus-iq\n    type: nexusIq\n    enable: false\n    user:             # Mandatory. User name\n    password:         # Mandatory. User password\n    url:              # Mandatory. Url of Nexus IQ server\n    organization-id:  # Mandatory. Organization UID like \"222de33e8005408a844c12eab952c9b0\"\n\n  - name: my-dependencytrack\n    type: dependencytrack\n    enable: false\n    url:                       # Mandatory. Url of Dependency Track\n    dependency-track-api-key:  # Mandatory. API key of Dependency Track\n\n  - name: my-opsgenie\n    type: opsgenie\n    enable: false\n    token: <API Key>  # Mandatory. an API key from an API integration\n    user:             # Optional. Display name of the request owner.\n    assignee:         # Optional. Comma separated list of users that the alert will be routed to send notifications\n    recipients: [\"\"]  # Optional. Comma separated list of users that the alert will become visible to without sending any notification\n    tags:             # Optional. Comma separated list of the alert tags.\n    priority:         # Optional. Specify the alert priority. Default is \"P3\"\n    alias:            # Optional. Client-defined identifier of the alert.\n    entity:           # Optional. Entity field of the alert that is generally used to specify which domain alert is related to.\n\nposteUi:\n  port: 8000\n  user: \"postee\"\n  pass: \"changeme\"\n  ## Use an existing secret\n  existingSecret:\n    enabled: false\n    # secretName: nameofsecret\n    # usernameKey: username\n    # passwordKey: password\n  image: aquasec/postee-ui\n  # By default `tag` is taken from `.Chart.AppVersion`\n  # To use different version - uncomment this line and enter the desired version\n  # tag: \"\"\n\nconfiguration:\n  # If set to true, ensure the externally generated secret to be named\n  # postee-secret and that it contains the JSON under a key called \"cfg.yaml\"\n  existingSecret:\n    enabled: false\n\nimage:\n  repository: aquasec/postee\n  pullPolicy: Always\n  # By default `tag` is taken from `.Chart.AppVersion`\n  # To use different version - uncomment this line and enter the desired version\n  # tag: \"\"\nimageInit:\n  repository: busybox\n  pullPolicy: IfNotPresent\n  tag: \"1.34\"\n\nimagePullSecrets: []\nnameOverride: \"\"\nfullnameOverride: \"\"\n\nserviceAccount:\n  # Specifies whether a service account should be created\n  create: true\n  # Annotations to add to the service account\n  annotations: {}\n  name: \"\"\n\npodAnnotations: {}\n\npodSecurityContext: {}\n  # fsGroup: 2000\n\nsecurityContext: {}\n  # capabilities:\n  #   drop:\n  #   - ALL\n  # readOnlyRootFilesystem: true\n  # runAsNonRoot: true\n  # runAsUser: 1000\n\nservice:\n  type: ClusterIP\n  portSsl: 8445\n  targetPortSsl: 8445\n  port: 8082\n  targetPort: 8082\n\nuiService:\n  type: LoadBalancer\n  port: 8000\n  targetPort: 8000\n\ningress:\n  enabled: false\n  annotations: {}\n    # kubernetes.io/ingress.class: nginx\n    # kubernetes.io/tls-acme: \"true\"\n  hosts:\n    - host: chart-example.local\n      paths:\n      - path: /\n        pathType: Prefix\n        backend:\n          serviceName: chart-example.local\n          servicePort: 80\n  tls: []\n\nresources:\n  limits:\n    cpu: 500m\n    memory: 256Mi\n  requests:\n    cpu: 200m\n    memory: 128Mi\n\nautoscaling:\n  enabled: false\n  minReplicas: 1\n  maxReplicas: 100\n  targetCPUUtilizationPercentage: 80\n  # targetMemoryUtilizationPercentage: 80\n\nnodeSelector: {}\n\ntolerations: []\n\naffinity: {}\n\n## Enable persistence using Persistent Volume Claims\n## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/\n##\npersistentVolume:\n  enabled: false\n  mountPathDb: /server/database\n  mountPathConfig: /data\n  mountPathRego: /server/rego-templates/custom\n  mountPathFilters: /server/rego-filters/custom\n  accessModes:\n    - ReadWriteOnce\n  size: 1Gi\n  annotations: {}\n  ## Persistent Volume Storage Class\n  ## If defined, storageClassName: <storageClass>\n  ## If set to \"-\", storageClassName: \"\", which disables dynamic provisioning\n  ## If undefined (the default) or set to null, no storageClassName spec is\n  ##   set, choosing the default provisioner.  (gp2 on AWS, standard on\n  ##   GKE, AWS & OpenStack)\n  ##\n  # storageClass: \"-\"\n  # existingClaim: \"\"\n\n## Secrets as environment variables\n## If defined, these secrets are mounted as environment variables\n## envFrom:\n##   - my-env-secret-1\n# Extra Volumes to add to the postee Statefulset\nextraVolumes: []\n#   - name: example_exec_env\n#     secret:\n#       defaultMode: 420\n#       secretName: example_exec_script\n\n# Extra Volumes Mounts to add to the postee Statefulset\nextraVolumeMounts: []\n#   - name: example_exec_env\n#     mountPath: /actions/exec/example_exec_env\n#     subPath: example_exec_env\n#     readOnly: true\n\n"
  },
  {
    "path": "deploy/kubernetes/hostPath/postee-pv.yaml",
    "content": "#Create the volume for the Postee volumeClaimTemplates\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: postee-db\n  labels:\n    app: postee-db\nspec:\n  capacity:\n    storage: 1Gi\n  accessModes:\n    - ReadWriteOnce\n  persistentVolumeReclaimPolicy: Retain\n  hostPath:\n    path: /tmp/postee/db/\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: postee-config\n  labels:\n    app: postee-config\nspec:\n  capacity:\n    storage: 100Mi\n  accessModes:\n    - ReadWriteOnce\n  persistentVolumeReclaimPolicy: Retain\n  hostPath:\n    path: /tmp/postee/config/\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: postee-rego-templates\n  labels:\n    app: postee-rego-templates\nspec:\n  capacity:\n    storage: 100Mi\n  accessModes:\n    - ReadWriteOnce\n  persistentVolumeReclaimPolicy: Retain\n  hostPath:\n    path: /tmp/postee/rego-templates/\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n  name: postee-rego-filters\n  labels:\n    app: postee-rego-filters\nspec:\n  capacity:\n    storage: 100Mi\n  accessModes:\n    - ReadWriteOnce\n  persistentVolumeReclaimPolicy: Retain\n  hostPath:\n    path: /tmp/postee/rego-filters/"
  },
  {
    "path": "deploy/kubernetes/postee-actions.yaml",
    "content": "## postee-configmap\n---\nkind: ConfigMap\napiVersion: v1\nmetadata:\n  name: postee-config\ndata:\n  cfg.yaml: |\n    ---\n    # The configuration file contains a general settings section,\n    # routes, templates and actions sections.\n\n    name: tenant            #  The tenant name\n    aqua-server:            #  URL of Aqua Server for links. E.g. https://myserver.aquasec.com\n    max-db-size: 1000       #  Max size of DB in MB. if empty then unlimited\n    db-verify-interval: 1   #  How often to check the DB size. By default, Postee checks every 1 hour\n\n    # Routes are used to define how to handle an incoming message\n    routes:\n    - name: stdout\n      actions: [ stdout ]\n      template: raw-json\n\n    - name: actions-route\n      input: contains(input.SigMetadata.ID, \"TRC-2\")\n      serialize-actions: true                       # Optional. Serialize actions in route.\n      actions: [save-tracee-event-log, send-event, tag-k8s-resources, kill-vulnerable-pod]\n      template: raw-json\n\n    # Templates are used to format a message\n    templates:\n    - name: raw-json                        # route message \"As Is\" to external webhook\n      rego-package: postee.rawmessage.json\n\n    # Actions are target services that should consume the messages\n    actions:\n    - name: stdout\n      type: stdout\n      enable: true\n\n    # Define a custom action of exec type, that can take params.\n    - name: save-tracee-event-log\n      type: exec\n      enable: true\n      env: [\"MY_ENV_VAR=foo_bar_baz\", \"MY_KEY=secret\"]\n      exec-script: |    # We create a persistent volume \"/postee-artifacts/\" to store logs\n        #!/bin/sh\n        echo $POSTEE_EVENT >> /postee-artifacts/tracee.event.log\n        \n    - name: send-event\n      type: http\n      enable: true\n      url: \"https://url-to-webhook.com\"     # Required. URL of the HTTP Request\n      method: POST                          # Required. Method to use. CONNECT is not supported at this time\n      headers:                              # Optional. Headers to pass in for the request.\n        \"Foo\": [\"bar\", \"baz\"]\n      timeout: 10s                          # Optional. Timeout value in XX(s,m,h)\n      body-content: |                       # Optional. Body of the HTTP request\n        This is an example of sending a Postee Event\n        via an HTTP Action. \n        \n        Event Details: \n          ID: event.input.SigMetadata.ID\n          Date: event.input.SigMetadata.Hostname\n        \n    - name: create-jira-ticket\n      type: jira \n      enable: false\n      url: https://foo-bar.atlassian.com\n      project-key: XYZ                     # Required. Specify the JIRA product key\n      user: johnwick@example.com           # Required. E.g :johndoe@gmail.com\"\n      password: hunter2                    # Optional. Specify Jira user API key. Used only for Jira Cloud\n      board:  \"postee-actions\"             # Optional. Specify the Jira board name to open tickets on\n      labels: [\"vulnerability\"]            # Optional, specify array of labels to add to Ticket, for example: [\"label1\", \"label2\"]\n      priority:  \"High\"                    # Optional. Specify the issues severity. Default is \"High\"\n      assignee:  [\"devops@example.com\"]    # Optional. Specify the assigned user. Default is the user that opened the ticket\n\n    - name: tag-k8s-resources\n      type: kubernetes\n      enable: true\n      kube-namespace: \"default\"                           # Required. Kubernetes namespace to use.\n      kube-label-selector: \"statefulset.kubernetes.io/pod-name=event.input.SigMetadata.Hostname\"               # Required, if specifying labels or annotations.\n      kube-actions:\n        labels:\n          category: \"vulnerability\"                        # Required. Label to add.\n          id: event.input.Vulnerability.ID               # Optional. It is also possible to add labels based on event inputs.\n          severity: event.input.Vulnerability.Severity\n    \n    - name: kill-vulnerable-pod\n      type: exec\n      enable: true\n      exec-script: |\n        #!/bin/sh\n        PODNAME=$(echo $POSTEE_EVENT | jq -r .SigMetadata.Hostname)    \n    \n        curl -k -X DELETE \\\n        -H \"Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)\" \\\n        https://kubernetes/api/v1/namespaces/default/pods/$PODNAME\n        \n## postee\n---\napiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: postee\n  name: postee-svc\nspec:\n  ports:\n  - name: https\n    port: 8445\n    protocol: TCP\n    targetPort: 8445\n  - name: http\n    port: 8082\n    protocol: TCP\n    targetPort: 8082\n  selector:\n    app: postee\n  type: ClusterIP\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  labels:\n    app: postee\n  name: postee\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: postee\n  serviceName: postee-svc\n  volumeClaimTemplates:\n  - metadata:\n      name: postee-db\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 1Gi\n  - metadata:\n      name: postee-artifacts\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  - metadata:\n      name: postee-config\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  - metadata:\n      name: rego-templates\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  - metadata:\n      name: rego-filters\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  template:\n    metadata:\n      labels:\n        app: postee\n      name: postee\n    spec:\n      initContainers:\n      - name: setting-db\n        image: busybox:1.34\n        command:\n        - /bin/chown\n        - -R\n        - \"1099:1099\"\n        - /server\n        volumeMounts:\n        - name: postee-db\n          mountPath: /server/database\n        - name: rego-templates\n          mountPath: /server/rego-templates/custom\n        - name: rego-filters\n          mountPath: /server/rego-filters/custom\n      - name: setting-cfg\n        image: busybox:1.34\n        command:\n        - cp\n        - /configmap/cfg.yaml\n        - /config/cfg.yaml\n        volumeMounts:\n        - name: configmap-vol\n          mountPath: /configmap\n          readOnly: false\n        - name: postee-config\n          mountPath: /config\n      containers:\n      - image: postee-local:latest\n        imagePullPolicy: IfNotPresent\n        name: postee\n        env:\n        - name: POSTEE_CFG\n          value: /config/cfg.yaml\n        - name: POSTEE_DEBUG\n          value: \"not\"\n        ports:\n        - name: http\n          containerPort: 8082\n          protocol: TCP\n        - name: tls\n          containerPort: 8445\n          protocol: TCP\n        volumeMounts:\n        - name: configmap-vol\n          mountPath: /configmap\n          readOnly: false\n        - name: postee-db\n          mountPath: /server/database\n        - name: postee-config\n          mountPath: /config\n        - name: rego-templates\n          mountPath: /server/rego-templates/custom\n        - name: rego-filters\n          mountPath: /server/rego-filters/custom\n        - name: postee-artifacts\n          mountPath: /postee-artifacts\n        securityContext:\n          allowPrivilegeEscalation: false\n          runAsNonRoot: true\n          runAsUser: 1099\n          runAsGroup: 1099\n        resources:\n          limits:\n            cpu: 500m\n            memory: 256Mi\n          requests:\n            cpu: 200m\n            memory: 128Mi\n        livenessProbe:\n          httpGet:\n            path: /ping\n            port: 8082\n          initialDelaySeconds: 10\n          periodSeconds: 10\n        readinessProbe:\n          httpGet:\n            path: /ping\n            port: 8082\n          initialDelaySeconds: 10\n          periodSeconds: 10\n      volumes:\n      - name: configmap-vol\n        configMap:\n          name: postee-config\n          items:\n          - key: cfg.yaml\n            path: cfg.yaml\n\n## postee-ui\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: postee-ui-svc\nspec:\n  ports:\n  - name: postee-ui\n    port: 8000\n    protocol: TCP\n    targetPort: 8000\n  selector:\n    app: postee-ui\n  type: LoadBalancer\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: postee-ui\n  name: postee-ui\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: postee-ui\n  template:\n    metadata:\n      labels:\n        app: postee-ui\n    spec:\n      initContainers:\n      - name: chmod-er\n        image: busybox:1.34\n        command:\n        - /bin/chown\n        - -R\n        - \"1099:1099\"\n        - /config\n        volumeMounts:\n        - name: postee-config\n          mountPath: /config\n      containers:\n      - image: aquasec/postee-ui:latest\n        imagePullPolicy: Always\n        name: postee-ui\n        env:\n        - name: POSTEE_UI_CFG\n          value: /config/cfg.yaml\n        - name: POSTEE_UI_PORT\n          value: \"8000\"\n        - name: POSTEE_UI_UPDATE_URL\n          value: \"http://postee-svc:8082\"\n        - name: POSTEE_ADMIN_USER\n          value: admin\n        - name: POSTEE_ADMIN_PASSWORD\n          value: admin\n        ports:\n        - containerPort: 8000\n          protocol: TCP\n        volumeMounts:\n        - name: postee-db\n          mountPath: /server/database\n        - name: postee-config\n          mountPath: /config\n        securityContext:\n          allowPrivilegeEscalation: false\n          runAsNonRoot: true\n          runAsUser: 1099\n          runAsGroup: 1099\n        resources:\n          limits:\n            cpu: 500m\n            memory: 256Mi\n          requests:\n            cpu: 200m\n            memory: 128Mi\n        livenessProbe:\n          httpGet:\n            path: /ping\n            port: 8000\n          initialDelaySeconds: 10\n          periodSeconds: 10\n        readinessProbe:\n          httpGet:\n            path: /ping\n            port: 8000\n          initialDelaySeconds: 10\n          periodSeconds: 10\n      volumes:\n      - name: postee-db\n        persistentVolumeClaim:\n          claimName: postee-db-postee-0\n      - name: postee-config\n        persistentVolumeClaim:\n          claimName: postee-config-postee-0\n"
  },
  {
    "path": "deploy/kubernetes/postee-controller.yaml",
    "content": "## postee-configmap\n---\nkind: ConfigMap\napiVersion: v1\nmetadata:\n  name: postee-controller-config\ndata:\n  cfg.yaml: |\n    ---\n    actions:\n    - type: stdout\n      name: stdout\n      enable: true\n    \n    - name: my-http-post-from-controller\n      type: http\n      enable: true\n      url: \"https://webhook.site/<uuid>\"       # Required. URL of the HTTP Request\n      method: POST                          # Required. Method to use. CONNECT is not supported at this time\n      headers:                              # Optional. Headers to pass in for the request.\n        \"Foo\": [ \"bar\" ]\n      timeout: 10s                          # Optional. Timeout value in XX(s,m,h)\n      body-content: |                       # Optional. Body inline content of the HTTP request\n        This is an example of a inline body\n        Input Image: event.input.image\n\n    - name: my-exec-from-runner\n      runs-on: \"postee-runner\"\n      type: exec\n      enable: true\n      env: [\"MY_ENV_VAR=foo_bar_baz\", \"MY_KEY=secret\"]     # Optional. Any environment variables to pass in\n      exec-script: |                                       # Specify the script to run\n        #!/bin/sh\n        echo $POSTEE_EVENT\n        echo \"this is hello from postee\"\n      \n    routes:\n    - name: stdout\n      actions: [ stdout ]\n      template: raw-json\n\n    - name: controller-only-route\n      input: contains(input.image, \"alpine\")\n      actions: [my-http-post-from-controller]\n      template: raw-json\n\n    - name: runner-only-route\n      input: contains(input.SigMetadata.ID, \"TRC-1\")\n      serialize-actions: true\n      actions: [my-exec-from-runner, my-http-post-from-runner]\n      template: raw-json\n\n    - name: controller-runner-route\n      input: contains(input.SigMetadata.ID, \"TRC-2\")\n      serialize-actions: true     # Cannot be strictly guaranteed as executions happen independently on runner/controller\n      actions: [my-exec-from-runner, my-http-post-from-runner, my-http-post-from-controller]\n      template: raw-json\n    \n    templates:\n    - name: raw-json                        # route message \"As Is\" to external webhook\n      rego-package: postee.rawmessage.json\n\n  server-cert.pem: |\n    -----BEGIN CERTIFICATE-----\n    -----END CERTIFICATE-----\n  server-key.pem: |\n    -----BEGIN PRIVATE KEY-----\n    ----END PRIVATE KEY-----\n  seed-file.txt: |\n    SUAGAA3TNI36JHTD6GLFJRR6KZIY7YXS2ZISHQA4LPZZZG2D6KG5JPV7DM\n    UBUQ63VFZEW3IS7RGQQZF5DIT2FTCMTZAAHFENK3G5M6ADRZ5WAJLAQN\n  root-ca.pem: |\n    -----BEGIN CERTIFICATE-----\n    -----END CERTIFICATE-----\n\n\n\n## postee\n---\napiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: postee-controller\n  name: postee-controller-svc\nspec:\n  ports:\n  - name: https\n    port: 8445\n    protocol: TCP\n    targetPort: 8445\n  - name: http\n    port: 8082\n    protocol: TCP\n    targetPort: 8082\n  - name: nats\n    port: 4222\n    protocol: TCP\n    targetPort: 4222\n  selector:\n    app: postee-controller\n  type: ClusterIP\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  labels:\n    app: postee-controller\n  name: postee-controller\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: postee-controller\n  serviceName: postee-controller-svc\n  volumeClaimTemplates:\n  - metadata:\n      name: postee-controller-db\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 1Gi\n  - metadata:\n      name: postee-controller-config\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  - metadata:\n      name: rego-templates\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  - metadata:\n      name: rego-filters\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  template:\n    metadata:\n      labels:\n        app: postee-controller\n      name: postee-controller\n    spec:\n      initContainers:\n      - name: setting-db\n        image: busybox:1.34\n        command:\n        - /bin/chown\n        - -R\n        - \"1099:1099\"\n        - /server\n        volumeMounts:\n        - name: postee-controller-db\n          mountPath: /server/database\n        - name: rego-templates\n          mountPath: /server/rego-templates/custom\n        - name: rego-filters\n          mountPath: /server/rego-filters/custom\n      - name: setting-cfg\n        image: busybox:1.34\n        command:\n        - cp\n        - /configmap/cfg.yaml\n        - /config/cfg.yaml\n        volumeMounts:\n        - name: configmap-vol\n          mountPath: /configmap\n          readOnly: false\n        - name: postee-controller-config\n          mountPath: /config\n      - name: controller-cfg\n        image: busybox:1.34\n        command: ['sh', '-c', 'cp /configmap/server-cert.pem /config/server-cert.pem && cp /configmap/server-key.pem /config/server-key.pem && cp /configmap/seed-file.txt /config/seed-file.txt && cp /configmap/root-ca.pem /config/root-ca.pem']\n        volumeMounts:\n        - name: configmap-vol\n          mountPath: /configmap\n          readOnly: false\n        - name: postee-controller-config\n          mountPath: /config\n      containers:\n      - image: aquasec/postee:latest\n        imagePullPolicy: Always\n        name: postee-controller\n        args:\n        - \"--controller-mode\"\n        - \"--controller-tls-cert\"\n        - \"/config/server-cert.pem\"\n        - \"--controller-tls-key\"\n        - \"/config/server-key.pem\"\n        - \"--controller-ca-root\"\n        - \"/config/root-ca.pem\"\n        - \"--controller-seed-file\"\n        - \"/config/seed-file.txt\"\n        env:\n        - name: POSTEE_CFG\n          value: /config/cfg.yaml\n        ports:\n        - name: http\n          containerPort: 8082\n          protocol: TCP\n        - name: tls\n          containerPort: 8445\n          protocol: TCP\n        - name: nats\n          containerPort: 4222\n          protocol: TCP\n        volumeMounts:\n        - name: configmap-vol\n          mountPath: /configmap\n          readOnly: false\n        - name: postee-controller-db\n          mountPath: /server/database\n        - name: postee-controller-config\n          mountPath: /config\n        - name: rego-templates\n          mountPath: /server/rego-templates/custom\n        - name: rego-filters\n          mountPath: /server/rego-filters/custom\n        securityContext:\n          allowPrivilegeEscalation: false\n          runAsNonRoot: true\n          runAsUser: 1099\n          runAsGroup: 1099\n        resources:\n          limits:\n            cpu: 500m\n            memory: 256Mi\n          requests:\n            cpu: 200m\n            memory: 128Mi\n        livenessProbe:\n          httpGet:\n            path: /ping\n            port: 8082\n          initialDelaySeconds: 10\n          periodSeconds: 10\n        readinessProbe:\n          httpGet:\n            path: /ping\n            port: 8082\n          initialDelaySeconds: 10\n          periodSeconds: 10\n      volumes:\n      - name: configmap-vol\n        configMap:\n          name: postee-controller-config\n          items:\n          - key: cfg.yaml\n            path: cfg.yaml\n          - key: server-cert.pem\n            path: server-cert.pem\n          - key: server-key.pem\n            path: server-key.pem\n          - key: seed-file.txt\n            path: seed-file.txt\n          - key: root-ca.pem\n            path: root-ca.pem\n## postee-ui\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: postee-ui-svc\nspec:\n  ports:\n  - name: postee-ui\n    port: 8000\n    protocol: TCP\n    targetPort: 8000\n  selector:\n    app: postee-ui\n  type: LoadBalancer\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: postee-ui\n  name: postee-ui\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: postee-ui\n  template:\n    metadata:\n      labels:\n        app: postee-ui\n    spec:\n      initContainers:\n      - name: chmod-er\n        image: busybox:1.34\n        command:\n        - /bin/chown\n        - -R\n        - \"1099:1099\"\n        - /config\n        volumeMounts:\n        - name: postee-controller-config\n          mountPath: /config\n      containers:\n      - image: aquasec/postee-ui:latest\n        imagePullPolicy: Always\n        name: postee-ui\n        env:\n        - name: POSTEE_UI_CFG\n          value: /config/cfg.yaml\n        - name: POSTEE_UI_PORT\n          value: \"8000\"\n        - name: POSTEE_UI_UPDATE_URL\n          value: \"http://postee-controller-svc:8082\"\n        - name: POSTEE_ADMIN_USER\n          value: admin\n        - name: POSTEE_ADMIN_PASSWORD\n          value: admin\n        ports:\n        - containerPort: 8000\n          protocol: TCP\n        volumeMounts:\n        - name: postee-controller-db\n          mountPath: /server/database\n        - name: postee-controller-config\n          mountPath: /config\n        securityContext:\n          allowPrivilegeEscalation: false\n          runAsNonRoot: true\n          runAsUser: 1099\n          runAsGroup: 1099\n        resources:\n          limits:\n            cpu: 500m\n            memory: 256Mi\n          requests:\n            cpu: 200m\n            memory: 128Mi\n        livenessProbe:\n          httpGet:\n            path: /ping\n            port: 8000\n          initialDelaySeconds: 10\n          periodSeconds: 10\n        readinessProbe:\n          httpGet:\n            path: /ping\n            port: 8000\n          initialDelaySeconds: 10\n          periodSeconds: 10\n      volumes:\n      - name: postee-controller-db\n        persistentVolumeClaim:\n          claimName: postee-controller-db-postee-0\n      - name: postee-controller-config\n        persistentVolumeClaim:\n          claimName: postee-controller-config-postee-0"
  },
  {
    "path": "deploy/kubernetes/postee-runner.yaml",
    "content": "## postee-configmap\n---\nkind: ConfigMap\napiVersion: v1\nmetadata:\n  name: postee-runner-config\ndata:\n  client-cert.pem: |\n    -----BEGIN CERTIFICATE-----\n    -----END CERTIFICATE-----\n  client-key.pem: |\n    -----BEGIN PRIVATE KEY-----\n    -----END PRIVATE KEY-----\n  seed-file.txt: |\n    SUAGAA3TNI36JHTD6GLFJRR6KZIY7YXS2ZISHQA4LPZZZG2D6KG5JPV7DM\n    UBUQ63VFZEW3IS7RGQQZF5DIT2FTCMTZAAHFENK3G5M6ADRZ5WAJLAQN\n  root-ca.pem: |\n    -----BEGIN CERTIFICATE-----\n    -----END CERTIFICATE-----\n## postee\n---\napiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: postee-runner\n  name: postee-runner-svc\nspec:\n  ports:\n  - name: https\n    port: 18445\n    protocol: TCP\n    targetPort: 18445\n  - name: http\n    port: 18082\n    protocol: TCP\n    targetPort: 18082\n  selector:\n    app: postee-runner\n  type: ClusterIP\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  labels:\n    app: postee-runner\n  name: postee-runner\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: postee-runner\n  serviceName: postee-svc\n  volumeClaimTemplates:\n  - metadata:\n      name: postee-runner-db\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 1Gi\n  - metadata:\n      name: postee-runner-config\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  - metadata:\n      name: rego-templates\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  - metadata:\n      name: rego-filters\n    spec:\n      accessModes: [ \"ReadWriteOnce\" ]\n      resources:\n        requests:\n          storage: 100Mi\n  template:\n    metadata:\n      labels:\n        app: postee-runner\n      name: postee-runner\n    spec:\n      initContainers:\n      - name: setting-db\n        image: busybox:1.34\n        command:\n        - /bin/chown\n        - -R\n        - \"1099:1099\"\n        - /server\n        volumeMounts:\n        - name: postee-runner-db\n          mountPath: /server/database\n        - name: rego-templates\n          mountPath: /server/rego-templates/custom\n        - name: rego-filters\n          mountPath: /server/rego-filters/custom\n      - name: controller-cfg\n        image: busybox:1.34\n        command: ['sh', '-c', 'cp /configmap/client-cert.pem /config/client-cert.pem && cp /configmap/client-key.pem /config/client-key.pem && cp /configmap/seed-file.txt /config/seed-file.txt && cp /configmap/root-ca.pem /config/root-ca.pem']\n        volumeMounts:\n        - name: configmap-vol\n          mountPath: /configmap\n          readOnly: false\n        - name: postee-runner-config\n          mountPath: /config\n      containers:\n      - image: aquasec/postee:latest\n        imagePullPolicy: Always\n        name: postee-runner\n        args:\n        - \"--runner-name\"\n        - \"postee-runner\"\n        - \"--controller-url\"\n        - \"tls://postee-controller-svc.default.svc.cluster.local:4222\"\n        - \"--runner-tls-cert\"\n        - \"/config/client-cert.pem\"\n        - \"--runner-tls-key\"\n        - \"/config/client-key.pem\"\n        - \"--runner-ca-root\"\n        - \"/config/root-ca.pem\"\n        - \"--runner-seed-file\"\n        - \"/config/seed-file.txt\"\n        - \"--url\"\n        - \"0.0.0.0:18082\"\n        - \"--tls\"\n        - \"0.0.0.0:18445\"\n        ports:\n        - name: http\n          containerPort: 18082\n          protocol: TCP\n        - name: tls\n          containerPort: 18445\n          protocol: TCP\n        volumeMounts:\n        - name: configmap-vol\n          mountPath: /configmap\n          readOnly: false\n        - name: postee-runner-db\n          mountPath: /server/database\n        - name: postee-runner-config\n          mountPath: /config\n        - name: rego-templates\n          mountPath: /server/rego-templates/custom\n        - name: rego-filters\n          mountPath: /server/rego-filters/custom\n        securityContext:\n          allowPrivilegeEscalation: false\n          runAsNonRoot: true\n          runAsUser: 1099\n          runAsGroup: 1099\n        resources:\n          limits:\n            cpu: 500m\n            memory: 256Mi\n          requests:\n            cpu: 200m\n            memory: 128Mi\n        livenessProbe:\n          httpGet:\n            path: /ping\n            port: 18082\n          initialDelaySeconds: 10\n          periodSeconds: 10\n        readinessProbe:\n          httpGet:\n            path: /ping\n            port: 18082\n          initialDelaySeconds: 10\n          periodSeconds: 10\n      volumes:\n      - name: configmap-vol\n        configMap:\n          name: postee-runner-config\n          items:\n          - key: client-cert.pem\n            path: client-cert.pem\n          - key: client-key.pem\n            path: client-key.pem\n          - key: seed-file.txt\n            path: seed-file.txt\n          - key: root-ca.pem\n            path: root-ca.pem"
  },
  {
    "path": "deploy/kubernetes/postee.yaml",
    "content": "## postee-configmap\n---\nkind: ConfigMap\napiVersion: v1\nmetadata:\n  name: postee-config\ndata:\n  cfg.yaml: |\n    ---\n    # Reference: https://github.com/aquasecurity/postee/blob/main/cfg.yaml\n\n    actions:\n      - type: email\n        name: my-email\n        user: xxxxxx@gmail.com\n        host: smtp.gmail.com\n        port: 587\n        sender: xxxxxxxx@gmail.com\n        recipients:\n          - xxxxxxxxx@xxxxx.com\n        password: xxxxxxxxxx\n        enable: false\n      - type: serviceNow\n        name: my-service-now\n        enable: false\n        user: xxxxxxxxx\n        password: xxxxxxxxxx\n        instance: xxxxxxxx\n      - type: slack\n        name: my-slack\n        enable: false\n        url: >-\n          https://hooks.slack.com/services/xxxxxxx/xxxxxxx/xxxxxxx\n      - type: teams\n        name: my-teams\n        enable: false\n        url: >-\n          https://xxxxxxxx.webhook.office.com/webhookb2/xxxxxxxx/IncomingWebhook/xxxxx/xxxxx\n    routes:\n      - name: slack-route\n        input: contains(input.image, \"alpine\")\n        actions:\n          - my-slack\n        template: legacy-slack\n        plugins:\n          aggregate-issues-number: null\n          aggregate-issues-timeout: null\n          policy-show-all: true\n        output: []\n      - output: []\n        name: email-route\n        input: |\n          contains(input.image,\"alpine\")\n        actions:\n          - my-email\n        template: legacy\n      - output: []\n        name: servicenow-route\n        input: contains(input.image,\"alpine\")\n        actions:\n          - my-service-now\n        template: legacy\n      - output: []\n        name: msTeams-route\n        actions:\n          - my-teams\n        template: legacy\n    templates:\n      - name: slack-template\n        rego-package: postee.vuls.slack\n      - name: rego-html\n        rego-package: postee.vuls.html\n      - name: legacy\n        legacy-scan-renderer: html\n      - name: legacy-slack\n        legacy-scan-renderer: slack\n      - name: custom-email\n        url: null\n      - name: trivy-operator-jira\n        rego-package: postee.trivyoperator.jira\n      - name: trivy-operator-slack\n        rego-package: postee.trivyoperator.slack\n    name: tenant\n    AquaServer: https://xxxxxxxxxxx.com\n\n## postee\n---\napiVersion: v1\nkind: Service\nmetadata:\n  labels:\n    app: postee\n  name: postee-svc\nspec:\n  ports:\n    - name: https\n      port: 8445\n      protocol: TCP\n      targetPort: 8445\n    - name: http\n      port: 8082\n      protocol: TCP\n      targetPort: 8082\n  selector:\n    app: postee\n  type: ClusterIP\n---\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n  labels:\n    app: postee\n  name: postee\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: postee\n  serviceName: postee-svc\n  volumeClaimTemplates:\n    - metadata:\n        name: postee-db\n      spec:\n        accessModes: [ \"ReadWriteOnce\" ]\n        resources:\n          requests:\n            storage: 1Gi\n    - metadata:\n        name: postee-config\n      spec:\n        accessModes: [ \"ReadWriteOnce\" ]\n        resources:\n          requests:\n            storage: 100Mi\n    - metadata:\n        name: rego-templates\n      spec:\n        accessModes: [ \"ReadWriteOnce\" ]\n        resources:\n          requests:\n            storage: 100Mi\n    - metadata:\n        name: rego-filters\n      spec:\n        accessModes: [ \"ReadWriteOnce\" ]\n        resources:\n          requests:\n            storage: 100Mi\n  template:\n    metadata:\n      labels:\n        app: postee\n      name: postee\n    spec:\n      initContainers:\n        - name: setting-db\n          image: busybox:1.34\n          command:\n            - /bin/chown\n            - -R\n            - \"1099:1099\"\n            - /server\n          volumeMounts:\n            - name: postee-db\n              mountPath: /server/database\n            - name: rego-templates\n              mountPath: /server/rego-templates/custom\n            - name: rego-filters\n              mountPath: /server/rego-filters/custom\n        - name: setting-cfg\n          image: busybox:1.34\n          command:\n            - cp\n            - /configmap/cfg.yaml\n            - /config/cfg.yaml\n          volumeMounts:\n            - name: configmap-vol\n              mountPath: /configmap\n              readOnly: false\n            - name: postee-config\n              mountPath: /config\n      containers:\n        - image: aquasec/postee:latest\n          imagePullPolicy: Always\n          name: postee\n          env:\n            - name: POSTEE_CFG\n              value: /config/cfg.yaml\n            - name: POSTEE_DEBUG\n              value: \"not\"\n          ports:\n            - name: http\n              containerPort: 8082\n              protocol: TCP\n            - name: tls\n              containerPort: 8445\n              protocol: TCP\n          volumeMounts:\n            - name: postee-db\n              mountPath: /server/database\n            - name: postee-config\n              mountPath: /config\n            - name: rego-templates\n              mountPath: /server/rego-templates/custom\n            - name: rego-filters\n              mountPath: /server/rego-filters/custom\n          securityContext:\n            allowPrivilegeEscalation: false\n            runAsNonRoot: true\n            runAsUser: 1099\n            runAsGroup: 1099\n          resources:\n            limits:\n              cpu: 500m\n              memory: 256Mi\n            requests:\n              cpu: 200m\n              memory: 128Mi\n          livenessProbe:\n            httpGet:\n              path: /ping\n              port: 8082\n            initialDelaySeconds: 10\n            periodSeconds: 10\n          readinessProbe:\n            httpGet:\n              path: /ping\n              port: 8082\n            initialDelaySeconds: 10\n            periodSeconds: 10\n      volumes:\n        - name: configmap-vol\n          configMap:\n            name: postee-config\n            items:\n            - key: cfg.yaml\n              path: cfg.yaml\n## postee-ui\n---\napiVersion: v1\nkind: Service\nmetadata:\n  name: postee-ui-svc\nspec:\n  ports:\n    - name: postee-ui\n      port: 8000\n      protocol: TCP\n      targetPort: 8000\n  selector:\n    app: postee-ui\n  type: LoadBalancer\n---\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n  labels:\n    app: postee-ui\n  name: postee-ui\nspec:\n  replicas: 1\n  selector:\n    matchLabels:\n      app: postee-ui\n  template:\n    metadata:\n      labels:\n        app: postee-ui\n    spec:\n      initContainers:\n        - name: chmod-er\n          image: busybox:1.34\n          command:\n            - /bin/chown\n            - -R\n            - \"1099:1099\"\n            - /config\n          volumeMounts:\n            - name: postee-config\n              mountPath: /config\n      containers:\n        - image: aquasec/postee-ui:latest\n          imagePullPolicy: Always\n          name: postee-ui\n          env:\n            - name: POSTEE_UI_CFG\n              value: /config/cfg.yaml\n            - name: POSTEE_UI_PORT\n              value: \"8000\"\n            - name: POSTEE_UI_UPDATE_URL\n              value: \"http://postee-svc:8082\"\n            - name: POSTEE_ADMIN_USER\n              value: admin\n            - name: POSTEE_ADMIN_PASSWORD\n              value: admin\n          ports:\n            - containerPort: 8000\n              protocol: TCP\n          volumeMounts:\n            - name: postee-db\n              mountPath: /server/database\n            - name: postee-config\n              mountPath: /config\n          securityContext:\n            allowPrivilegeEscalation: false\n            runAsNonRoot: true\n            runAsUser: 1099\n            runAsGroup: 1099\n          resources:\n            limits:\n              cpu: 500m\n              memory: 256Mi\n            requests:\n              cpu: 200m\n              memory: 128Mi\n          livenessProbe:\n            httpGet:\n              path: /ping\n              port: 8000\n            initialDelaySeconds: 10\n            periodSeconds: 10\n          readinessProbe:\n            httpGet:\n              path: /ping\n              port: 8000\n            initialDelaySeconds: 10\n            periodSeconds: 10\n      volumes:\n        - name: postee-db\n          persistentVolumeClaim:\n            claimName: postee-db-postee-0\n        - name: postee-config\n          persistentVolumeClaim:\n            claimName: postee-config-postee-0\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: \"3\"\nservices:\n    posteewebhook:\n        build:\n            context: .\n            dockerfile: Dockerfile\n        ports:\n            - 8082:8082\n        volumes:\n            - bolt:/server/database\n            - config:/server\n        environment:\n            - \"POSTEE_CFG=/server/cfg.yaml\"\n            - \"POSTEE_DEBUG=ON\"\n    posteeui:\n        build:\n            context: .\n            dockerfile: Dockerfile.ui\n        ports:\n            - 8001:8001\n        volumes:\n            - bolt:/server/database\n            - config:/server\n        environment:\n            - \"POSTEE_UI_CFG=/server/cfg.yaml\"\n            - \"POSTEE_UI_PORT=8001\"\n            - \"POSTEE_UI_UPDATE_URL=http://posteewebhook:8082\"\nvolumes:\n  bolt:\n  config:"
  },
  {
    "path": "docs/actions/actions.md",
    "content": "# Postee Actions\n\n## Motivation\nProper alert management can help security practitioners make informed decisions about their codebase. However, security alerts can cause fatigue if acting on them isn’t possible. Postee, an open source security alert management tool, helps mitigate some of those concerns. It enables teams to define routes and rules by which alerts are handled and redirected to \n\n## User Stories\nIn a typical Postee setup, users can configure the tool to receive events from a variety of sources over a webhook. This allows for ease of use in existing environments. Furthermore, users can configure Postee to process these incoming events and, based on logic defined via Rego rules, send them to different actions.\n\nAs a, **Postee User**\n- _I want_, to be able to remove a vulnerable image from my cluster upon a Trivy scan  \n_So that_, I can keep such images unavailable for deployment.\n\n\n- _I want_, to ship Tracee security notification logs from my node when events are detected   \n_So that_, I can build a timelog for forensics purposes.\n\n\n- _I want_, to be able to add labels to my deployments when Starboard detects a vulnerable image in my cluster   \n_So that_, I can effectively tag my resources.\n\n\n## Configuring Postee Actions\nIn this README, we’ll walk through a scenario in which a user wants to act on a security event received from Tracee, an open source runtime security tool. In this scenario, the user will set up the Postee Exec Action to save logs for forensic purposes and then use the Postee HTTP Action to ship the saved logs to a remote server.\n\nIn this case, the incoming security event from Tracee is received by Postee and evaluated by the following route YAML definition: \n\n![img.png](../img/img.png)\n\nAs seen above, the route has a Rego rule that evaluates the input to contain a certain signature ID, TRC-2, which represents anti-debugging activity. In addition, if the input is matched, the output is triggered.\n\n## Exec Action\n\nIn this case, we call the Exec Action first and then the HTTP Action. They are defined as the following:\n\nThe Exec Action can take in the following parameters:\n\n| Option      | Usage                                                                                     |\n|-------------|-------------------------------------------------------------------------------------------|\n| env         | Optional, custom environment variables to be exposed in the shell of the executing script |\n| input-file  | Required, custom shell script to executed                                                 |\n| exec-script | Required, inline shell script executed                                                    |\n\nThe Exec Action also internally exposes the `$POSTEE_EVENT` environment variable with the input event that triggered the action. This can be helpful in situations where the event itself contains useful information.\n\nBelow is an example of using `$POSTEE_EVENT`. It uses the inline exec-script script:\n\n![img_3.png](../img/img_3.png)\n\nAs you can see, we capture the incoming Postee event and write this event to the Tracee event log for forensic purposes.\n\n## HTTP Action\n\nFinally, we can configure the Postee HTTP Post Action to ship the captured event logs via our HTTP Action to our remote server.\n\n ![img_1.png](../img/img_1.png)\n\n| Option   | Usage                                   |\n|----------|-----------------------------------------|\n| URL      | Required, URL of the remote server      |\n| Method   | Required, e.g., GET, POST               |\n| Headers  | Optional, custom headers to send        |\n| Timeout  | Optional, custom timeout for HTTP call  |\n| Bodyfile | Optional, input file for HTTP post body |\n\nTo run Postee in the container, we can invoke the Postee Docker container:\n\n```\ndocker run --rm --name=postee \\\n-v <path-to-cfg>:/config/cfg-actions.yaml  \\\n-e POSTEE_CFG=/config/cfg-actions.yaml \\\n-e POSTEE_HTTP=0.0.0.0:8084  \\\n-e POSTEE_HTTPS=0.0.0.0:8444  \\\n-p 8084:8084 -p 8444:8444 aquasecurity/postee:latest\n```\n\n## Kubernetes Action\nIn addition to the Exec and HTTP actions, we have also implemented a Kubernetes action that today can add labels and annotations to pods. It can be used as follows:\n\n![img_4.png](../img/img_4.png)\n\n| Option              | Usage                                                                                                                           |\n|---------------------|---------------------------------------------------------------------------------------------------------------------------------|\n| kube-namespace      | Required. Kubernetes namespace to use.                                                                                          |\n| kube-config-file    | Required. Path to .kubeconfig file                                                                                              |\n| kube-label-selector | Required, if specifying labels or annotations.                                                                                  |\n| kube-actions        | Optional, key-value pair of labels and annotations<br/>Labels must be added via \"labels\" key and Annotations via \"annotations\". |\n\n\n## Docker Action\nWe have also added a Docker Action, that can help you run docker images as an action within a container.\n\n![img_5.png](../img/img_5.png)\n\n| Option               | Usage                                                                                                                                                                    |\n|----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| docker-image-name    | Required. Image name of the docker image.                                                                                                                                |\n| docker-cmd           | Required. Command to run inside the docker image.                                                                                                                        |\n| docker-env           | Optional. Environment variables to set in the container.                                                                                                                 |\n| docker-network       | Optional. Connect the action container to the specified network. {e.g. \"host\"}                                                                                           |\n| docker-volume-mounts | Optional*. Volume mounts present inside the container.<br/> * _If you have specified volume mounts, you also need to pass them through into the postee docker container_ |\n\n### Note\nWhen running Postee in a Docker container, it is required to mount the Docker socket within the Postee container to be able to spin up Docker Action container instances. This can be done as follows:\n```\ndocker run --rm --name=postee --group-add $(stat -c '%g' /var/run/docker.sock) -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cfg.yaml:/config/cfg.yaml  -e POSTEE_CFG=/config/cfg.yaml -e POSTEE_HTTP=0.0.0.0:8084     -e POSTEE_HTTPS=0.0.0.0:8444     -p 8084:8084 -p 8444:8444 aquasecurity/postee:latest\n```\n\nIf you have specified volume mounts for a docker container and use Postee in a docker container as well, remember to mount them within the Postee container as well:\n```\ndocker run --rm --name=postee --group-add $(stat -c '%g' /var/run/docker.sock) -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cfg.yaml:/config/cfg.yaml  -v /my/custom/volume:/my/custom/volume -e POSTEE_CFG=/config/cfg.yaml -e POSTEE_HTTP=0.0.0.0:8084     -e POSTEE_HTTPS=0.0.0.0:8444     -p 8084:8084 -p 8444:8444 aquasecurity/postee:latest\n```"
  },
  {
    "path": "docs/actions.md",
    "content": "## Motivation\nProper alert management can help security practitioners make informed decisions about their codebase. However, security alerts can cause fatigue if acting on them isn’t possible. Postee, an open source security alert management tool, helps mitigate some of those concerns. It enables teams to define routes and rules by which alerts are handled and redirected to\n\n## User Stories\nIn a typical Postee setup, users can configure the tool to receive events from a variety of sources over a webhook. This allows for ease of use in existing environments. Furthermore, users can configure Postee to process these incoming events and, based on logic defined via Rego rules, send them to different actions.\n\nAs a **Postee User**\n\n- _I want_, to be able to remove a vulnerable image from my cluster upon a Trivy scan  \n  _So that_, I can keep such images unavailable for deployment.\n\n\n- _I want_, to ship Tracee security notification logs from my node when events are detected   \n  _So that_, I can build a timelog for forensics purposes.\n\n\n- _I want_, to be able to add labels to my deployments when Starboard detects a vulnerable image in my cluster   \n  _So that_, I can effectively tag my resources.\n\n![settings](img/postee-actions.png)\n\nActions are remote services that messages should be sent to. Each action has two mandatory fields, which are 'name' and 'type'.\n\nKey | Description | Values | Example\n--- | --- | --- | ---\n*name* | Unique name of the action. This name is used in the route definition. | Any string | teams-action\n*type* | The type of the action | You can choose from the following types: email, jira, slack, teams, webhook, splunk, serviceNow | email\n\n!!! tip \n      Depending on the 'type', additional parameters are required.\n\n## Jira\n\nFollow these steps to set up JIRA integration:\n\n1. Get a new token for user:\n    * Login to Jira Cloud.\n      Go to the user profile API tokens (JIRA Cloud users can find it [here](https://id.atlassian.com/manage-profile/security/api-tokens)).\n    * Click on the Create API Token. A new API token for the user is created.\n    * Login to Jira Server/Data center\n      Select your profile picture at top right of the screen, then choose  Settings > Personal Access Tokens. Select Create token. Give your new token a name. Optionally, for security reasons, you can set your token to automatically expire after a set number of days. Click Create. A new PAT for the user is created.\n2. Fill jira action in cfg.yaml:\n    * Jira Cloud:\n        * User: your email.\n        * Password: your API token.\n    * Jira Server/Data center:\n        * User: your UserName.\n        * Password: your Password.\\\n          or\n        * Token: your Personal Access Tokens.\n\nKey           | Description          | Values | Required \n--------------|----------------------|-----------------|----------\n*url*         | Jira project url     |                 | Yes\n*project-key* | The JIRA project key |                 | Yes\n*user*        | Jira user. Use email for Jira Cloud and UserName for Jira Server/Data Center    |             | Yes\n*password*    | User's password. API token can also be used for Cloud Jira instances. |             | No\n*token*       | User's Personal Access Token. Used only for Jira Server/Data Center   |             | No          \n*board*       | JIRA board key              |           | No\n*priority*    | ticket priority, e.g., High |           | No\n*assignee*    | comma separated list of users (emails) that will be assigned to ticket, e.g., [\"john@yahoo.com\"]. To assign a ticket to the Application Owner email address (as defined in Aqua Application Scope, owner email field), specify [\"<%application_scope_owner%>\"] as the assignee value   |               | No\n*issuetype*   | issue type, e.g., Bug        |           | No\n*labels*      | comma separated list of labels that will be assigned to ticket, e.g., [\"label1\", \"label2\"]|         | No\n*sprint*      | Sprint name, e.g., \"3.5 Sprint 8\" |      | No\n\nFor Jira you can also specify custom fields that will be populated with values.\nUse the `unknowns` parameter in cfg.yaml for custom fields.\nUnder the `unknowns` parameter, specify the list of fields **names** to provide value for. Field name can contains spaces.\n\nPossible options for getting the field name:\n\n??? note \"Get field name from Jira UI\"\n    1. Move to your jira.\n    2. Navigate to **Settings**(![cog](https://user-images.githubusercontent.com/91113035/159643662-b7a21717-58f0-4a5e-87a0-0d840046e215.png)) > **Issues** > **Custom fields** under the Fields section: ![Custom_fields](img/jira-custom_fields.png)\n    3. Click on the required field. ![Field_information](img/jira-field_information.png)\n    4. Get value from **Name** field.\n\n\n??? note \"Get field name from Jira REST API\"\n    1. Get all Jira fields [according to instructions](https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-fields/#api-rest-api-3-field-get)\n    2. Find needed field:\n     \n    ```\n     ...\n         \"id\": \"customfield_10014\",\n         \"key\": \"customfield_10014\",\n         \"name\": \"Epic Link\",\n         \"untranslatedName\": \"Epic Link\",\n         \"custom\": true,\n         \"orderable\": true,\n         \"navigable\": true,\n         \"searchable\": true,\n         \"clauseNames\": [\n           \"cf[10014]\",\n           \"Epic Link\"\n         ],\n         \"schema\": {\n           \"type\": \"any\",\n           \"custom\": \"com.pyxis.greenhopper.jira:gh-epic-link\",\n           \"customId\": 10014\n         }\n       },\n     ...\n    ```\n    3. Get value from **Name** field.\n\n    Example of using the `unknowns` parameter in the cfg.yaml file:\n\n    ```yaml\n    unknowns:\n         Epic Link: \"K8S-1\"\n    ```\n\n!!! tip\n      You can add \"-numeric-field\", \"-multiple-value\", \"multiple-line-text-field\", \"-date-time-picker\" and \"-field-url\" as suffix to the custom field name, to specify what is the field type.\n\n      For example:\n      ```yaml\n      unknowns:\n           mycustom: \"this is a text custom field\"\n           mycustom-numeric-field: 123\n           mycustom-multiple-value: 1,2,3\n           mycustom-multiple-line-text-field: \"text \\n moretext\"\n           mycustom-date-time-picker: 2014-04-11T12:14:26.880+0400\n           mycustom-url: https://tour.golang.org/moretypes/7\n      ```\n\n## Email\nKey          | Description | Values | Required \n-------------|-------------|-----------------|----------\n*use-mx*     | Whether to send the email as an SMTP server or a client. Specify 'true' if you would like to send email as an smtp server, in this case you don't need to provide user, password, host and port. | true, false |      \n*user*       | if auth supported. User name (usually email address) |      | No\n*password*   | if auth supported. Password |     | No\n*host*       | SMTP host name |          | Yes\n*port*       | SMTP port      |          | Yes\n*sender*     |  Sender's email address   |           | Yes\n*recipients* |  Recipients (array of comma separated emails), e.g. [\"john@yahoo.com\"]. To send the email to the Application Owner email address (as defined in Aqua Application Scope, owner email field), specify [\"<%application_scope_owner%>\"] as the recipients value |         | Yes\n\n## Slack\nGetting the Slack webhooks [Create a Slack Custom App](https://api.slack.com/messaging/webhooks).\n\nCopy webhook url to the Postee config\n\nKey | Description | Values\n--- | --- | ---\n*url* | Slack WebHook URL (includes the access key) |\n\n## MS Teams\n\nOpen your Microsoft Teams client. Click on the \"...\" near the channel you would like to send notifications to.\n\nChoose \"Connectors\". The connectors window will open. Look for the \"Incoming Webhook\" connector (it is under the \"All\" category).\n\nClick \"Add\" near the Incoming Webhook connector. Click \"Add\" again. Provide a name and click \"Create\".\n\nYou will be provided with a URL address. Copy this URL and put it in the cfg.yaml.\n\nKey | Description | Values\n--- | --- | ---\n*url* | MS Teams WebHook URL |\n\n## Splunk\n\nYou will need to care about an HTTP Event Collector in Splunk Enterprise or Splunk Cloud.\n\n!!! tip\n      This can usually be found in the Splunk console under \"Settings -> Data Inputs -> HTTP Event Collector -> Add New\".\n\nOnce you create an HTTP Event Collector you will receive a token. You should provide this token, together with the Splunk HTTP Collector\nURL, as part of the cfg.yaml settings.\n\nKey          | Description                                                         | Values | Required  \n------------ | ------------------------------------------------------------------  | --------------- | ---------             \n*token*      | The Splunk HTTP event collector token                               |                 | Yes\n*url*        | URL to Splunk HTTP event collector (e.g. http://server:8088)        |                 | Yes\n*size-limit* | Maximum scan length, in bytes. Default: 10000 | 10000     |                 | No\n\n## ServiceNow\n\nKey        | Description                                                        | Values  | Required  \n---------- | ------------------------------------------------------------------ | ---------------  | --------- \n*user*     | ServiceNow user name                                               |                  | Yes\n*password* | User API key / password                                            |                  | Yes\n*instance* | Name of ServiceNow Instance (usually the XXX at XXX.servicenow.com)|                  | Yes\n*board*    | ServiceNow board name to open tickets on. Default is \"incident\"    |                  |           \n\n## Nexus IQ\n\nKey               | Description                                              | Values | Required  \n----------------- | -------------------------------------------------------- | --------------- | --------  \n*user*            | Nexus IQ user name                                       |                 | Yes\n*password*        | Nexus IQ password                                        |                 | Yes\n*url*             | Url of Nexus IQ server                                   |                 | Yes\n*organization-id* | Organization UID like \"222de33e8005408a844c12eab952c9b0\" |                 | Yes\n\n## Dependency Track\n\nKey                        | Description                    | Values  | Required  \n-------------------------- | ------------------------------ | ------- | --------  \n*url*                      | Url of Dependency Track        |         | Yes\n*dependency-track-api-key* | API key of Dependency Track    |         | Yes\n\n## OpsGenie\n\n??? note \"Set up OpsGenie and get a token\"\n\n    1. Go to your Opsgenie and select Teams from menu.\n    2. Select your team to access your team dashboard.\n    3. Select Integrations from left navigation.\n    4. Select Add Integration.\n    5. Select API Integration.\n    6. Copy `API Key`.\n    7. When done with all configurations, select Save Integration to enable the integration.\n\n    See more details here: [Set up an integrated tool for Opsgenie](https://support.atlassian.com/opsgenie/docs/set-up-an-integrated-tool/).\n\n!!! caution\n    Postee requires an API key from an [API integration](https://support.atlassian.com/opsgenie/docs/what-is-a-default-api-integration/). This can be added under the Settings -> Integrations tab. Or it can under a team's Integrations tab.\n\n    If the integration assigns an alert to a team, it can only create alerts for that team.\n      \n    An API key from the `API Key Management` tab will produce an HTTP 403 error. This API Key is valid but cannot create alerts as it lacks necessary permissions. \n\nKey        | Description                             | Values | Required \n-----------| --------------------------------------- | ----------------|--------- \ntoken      | an API key from an API integration      |                 | Yes\nuser       | Display name of the request owner.      |                 | No\nassignee   | Comma separated list of users that the alert will be routed to send notifications                     |          | No\nrecipients | Comma separated list of users that the alert will become visible to without sending any notification  |          | No\npriority   | Specify the alert priority. Default is \"P3\" | \"P1\" \"P2\" \"P3\" \"P4\" \"P5\"| No\ntags       | Comma separated list of the alert tags.     |                         | No\nalias      | Client-defined identifier of the alert.     |                         | No\nentity     | Entity field of the alert that is generally used to specify which domain alert is related to. |        | No\n\n## Exec\n\n Option      | Usage                                                                                     | Required \n-------------|-------------------------------------------------------------------------------------------|----------\n env         | custom environment variables to be exposed in the shell of the executing script | No\n input-file  | custom shell script to executed                                                 | Yes\n exec-script | inline shell script executed                                                    | Yes\n\nThe Exec Action also internally exposes the `$POSTEE_EVENT` environment variable with the input event that triggered the action. This can be helpful in situations where the event itself contains useful information.\n\nBelow is an example of using `$POSTEE_EVENT`. It uses the inline exec-script script:\n\n![img_3.png](img/img_3.png)\n\n## HTTP\n\n![img_1.png](img/img_1.png)\n\n Option   | Usage                                   | Required \n----------|-----------------------------------------|----------\n URL      | URL of the remote server      | Yes\n Method   | e.g., GET, POST               | Yes\n Headers  | custom headers to send        | No\n Timeout  | custom timeout for HTTP call  | No\n Bodyfile | input file for HTTP post body | No\n\n\n## Kubernetes\n![img_4.png](img/img_4.png)\n\n Option              | Usage                                                                                                                           | Required \n---------------------|---------------------------------------------------------------------------------------------------------------------------------|----------\n kube-namespace      | Kubernetes namespace to use.                                                                                          | Yes\n kube-config-file    | Path to .kubeconfig file                                                                                              | Yes\n kube-label-selector | if specifying labels or annotations.                                                                                  | Yes\n kube-actions        | key-value pair of labels and annotations<br/>Labels must be added via \"labels\" key and Annotations via \"annotations\". | No\n\n\n## Docker\n![img_5.png](img/img_5.png)\n\n Option               | Usage                                                                                                                                                         | Required \n----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|----------\n docker-image-name    | Image name of the docker image.                                                                                                                                | Yes\n docker-cmd           | Command to run inside the docker image.                                                                                                                        | Yes\n docker-env           | Environment variables to set in the container.                                                                                                                 | No\n docker-network       | Connect the action container to the specified network. {e.g. \"host\"}                                                                                           | No\n docker-volume-mounts | *Volume mounts present inside the container.<br/> * _If you have specified volume mounts, you also need to pass them through into the postee docker container_ | No\n\n!!! note\n      When running Postee in a Docker container, it is required to mount the Docker socket within the Postee container to be able to spin up Docker Action container instances. This can be done as follows:\n      ```\n      docker run --rm --name=postee --group-add $(stat -c '%g' /var/run/docker.sock) -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cfg.yaml:/config/cfg.yaml  -e POSTEE_CFG=/config/cfg.yaml -e POSTEE_HTTP=0.0.0.0:8084     -e POSTEE_HTTPS=0.0.0.0:8444     -p 8084:8084 -p 8444:8444 aquasecurity/postee:latest\n      ```\n\n!!! tip\n      If you have specified volume mounts for a docker container and use Postee in a docker container as well, remember to mount them within the Postee container as well:\n      ```\n      docker run --rm --name=postee --group-add $(stat -c '%g' /var/run/docker.sock) -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cfg.yaml:/config/cfg.yaml  -v /my/custom/volume:/my/custom/volume -e POSTEE_CFG=/config/cfg.yaml -e POSTEE_HTTP=0.0.0.0:8084     -e POSTEE_HTTPS=0.0.0.0:8444     -p 8084:8084 -p 8444:8444 aquasecurity/postee:latest\n      ```\n\n\n## Generic Webhook\n\nKey | Description | Values\n--- | --- | ---\n*url* | Webhook URL |\n*timeout*  | Webhook timeout  |\n\n!!! tip\nThe generic webhook action can be used for sending Postee output to any endpoint that can receive a request. You can find some interesting examples as part of the [Postee Blueprints](/blueprints)\n\n## DefectDojo\n\nDefectDojo is a DevOpsSec and vulnerability management tool. When sending a Trivy operator report, the API expects us to send a multipart/form-data POST request to the API endpoint. Authentication is done through an API token that can be easily provided by either environment variables or K8s secrets.\n\nAt the time of writing, Postee doesn't provide any native action module targeting the DefectDojo API. Instead the solution is to apply a shell script through an EXEC action that consumes the JSON output of a custom made REGO template that mangles the JSON payload received from a Trivy operator instance.\n\nThe REGO template will be use-case specific because the metadata added heavily depends on the users setup and hierarchical structure inside the user's DefectDojo instance.\n\nThe resulting JSON data puts the Trivy report under the `report` key and derived meta data under the `metadata` key. The idea behind this is to provide a data structure that will make it easy to develop a more generic shell script. In a subsequent step an EXEC module is called consuming the resulting JSON structure from an environment variable called `POSTEE_EVENT`. For more information see the [EXEC action](#Exec).\n\n### Implementation\n\n1. DefectDojo - create an non-interactive API user and an API token\n2. Postee - deploy the token as `DEFECTDOJO_API_TOKEN` environment variable\n3. Postee - deploy the base URL for DefectDojo using `DEFECTDOJO_URL`\n4. Mount the [example shell script](../actions/example/exec/defectdojo-curl-upload-scan.sh) into the container\n5. Mount the [example rego template](../rego-templates/example/defectdojo/trivy-operator-defectdojo.rego) into the container\n6. Update your configuration according to the [example](../config/cfg-trivy-operator-defectdojo.yaml) provided\n7. Validate the setup by sending an example report in JSON format using the following shell command `curl -X POST -H \"Content-Type: application/json\" -d @trivy-operator-report.json http://postee:8082`\n"
  },
  {
    "path": "docs/advanced.md",
    "content": "This page covers some advanced topics that the experienced users of Postee might like to try. \n\n## Using environment variables in Postee Configuration File\nPostee supports use of environment variables for *Output* fields: **User**, **Password** and **Token**. \n\nAdd prefix `$` to the environment variable name in the configuration file, for example:\n```\nactions:\n- name: my-jira   \n  type: jira     \n  enable: true\n  user: $JIRA_USERNAME\n  token: $JIRA_SERVER_TOKEN         \n```\n\n### Helm\n\nWhen installing Postee on Kubernetes with Helm, you can provide environment variables from Kubernetes secrets.\nGiven there is a Secret containing sensitive information:\n```\napiVersion: v1\nkind: Secret\nmetadata:\n  name: mysecret\ntype: Opaque\ndata:\n  JIRA_USERNAME: secret-username\n  JIRA_SERVER_TOKEN: secret-token\n```\n\nYou can refer to this secret and use its data in Postee by specifying its name in the Helm values:\n```\nenvFrom:\n  - mysecret\n```\n\n## Customizing Templates\nPostee loads bundle of templates from `rego-templates` folder. This folder includes several templates shipped with Postee, which can be used out of the box. You can add additional custom templates by placing Rego file under the 'rego-templates' directory.\n\nTo create your own template, you should create a new file under the 'rego-templates' directory, and use the\n[Rego language](https://www.openpolicyagent.org/docs/latest/policy-language/) for the actual template code.\n\nMessage payload is referenced as `input` when template is rendered. The `result` variable should be used to store the output message, which is the result of the template formatting.\n\nThe following variables should be defined in the custom Rego template.\n\nKey | Description |Type\n--- | --- | ---\n*result* | message body| Can be either string or json\n*title* | message title| string\n*aggregation_pkg*|reference to package used to aggregate messages (when aggregate-message-timeout or aggregate-message-number options are used). If it's missed then aggregation feature is not supported| valid rego package\n\nSo the simplest example of Rego template would look like:\n```rego\npackage example.vuls.html\n\ntitle:=\"Vulnerabilities are found\"\nresult:=sprintf(\"Vulnerabilities are found while scanning of image: <i>%s</i>\", [input.image])\n```\n\nTwo examples are shipped with the app. One produces output for slack integration and another one builds html output which can be used across several integrations. These example can be used as starting point for message customization\n\n## Data Persistence\nThe Postee container uses BoltDB to store information about previously scanned images.\nThis is used to prevent resending messages that were already sent before.\nThe size of the database can grow over time. Every image that is saved in the database uses 20K of storage.\n\nPostee supports ‘PATH_TO_DB’ environment variable to change the database directory. To use, set the ‘PATH_TO_DB’ environment variable to point to the database file, for example: PATH_TO_DB=\"./database/webhook.db\". \n\nBy default, the directory for the database file is “/server/database/webhook.db”.\n\n!!! tip\n        If you would like to persist the database file between restarts of the Postee container, then you should use a persistent storage option to mount the \"/server/database\" directory of the container.\n\nThe \"deploy/kubernetes\" directory in this project contains an example deployment that includes a basic Host Persistency.\n    \n"
  },
  {
    "path": "docs/aquacloud.md",
    "content": "## Configure the Aqua Server with Webhook Integration\nPostee can be integrated with Aqua Console to deliver vulnerability and audit messages to target systems.\n\nYou can configure the Aqua Server to send a Webhook notification whenever a new vulnerability is found.\nNavigate to the **Image Scan Results Webhook** page, under the \"Settings\" menu.\n\n![Screenshot](img/webhook-integration.png)\n\nClick \"Enable sending image scan results to webhook\", and specify the URL of Postee.\nNow, scan an image and look at the Postee log files - you will see that Postee have received an incoming message once scan is done,\nand that the message was routed based on the cfg.yaml configuration.\n\nYou can also configure the Aqua Server to send a Webhook notification for every audit message.\nNavigate to the **Log Management** page, under the \"Integrations\" menu.\n\n![Screenshot](img/aqua-webhook-audit.jpg)\n\nClick on the \"Webhook\" item, and specify the URL of Postee.\n\nNow every audit event in Aqua will be sent to Postee. You can configure routes and input message conditions in Postee cfg.yaml to\nforward appropriate messages to target systems.\n\nThe **Postee URL** is in the following formats:\n\n> `https://<Postee IP or DNS>:8445`\n> `http://<Postee IP or DNS>:8082`\n\n!!! tip\n        For more details about the Postee URL installed using kubernetes [click here](./deploy/kubernetes/README.md)\n\n### Validate the Integration\n\nTo validate that the integration is working, you can scan a new image for security vulnerabilities from the Aqua Server UI (Images > Add Image > Specify Image Name > Add).\n\nWhen vulnerabilities are found in an image, you will see that a JIRA ticket is created/ Email is received/ Slack message is posted to the channel.\n\n!!! tip\n        To troubleshoot the integration, you can look at both the Aqua Postee container logs and the Aqua Server logs. Use the \"docker logs <container name>\" command to view these logs.*\n\n"
  },
  {
    "path": "docs/blueprints/devops-pagerduty.md",
    "content": "# Paging DevOps Teams\n\n## Introduction\nIn this walkthrough, we will setup vulnerability scanning with [Trivy](https://github.com/aquasecurity/trivy) and send the results to Postee for paging DevOps team members for critical vulnerabilities as they are introduced.\n\n## Scenario\nA DevOps team would like to configure alerts for scheduled vulnerability scans to notify them about any vulnerable images that they might be running in their clusters. For this they decide to install Trivy, run it on a schedule and send the results to Postee.\n\nThey decide to configure Postee so that upon receiving such alerts, Postee sends an event to PagerDuty which fires off an alert to inform DevOps teams to take necessary action. \n\n![img.png](assets/trivy-pagerduty.png)\n\n## Sample Configs\nIn this case a sample configuration for the components can be described as follows:\n\n### Postee Config\n```yaml\nroutes:\n- name: Trivy Alerts to Pagerduty\n  input: input.report.summary.criticalCount > 0\n  actions: [alert-devops]\n  template: trivy-raw-json\n\n# Templates are used to format a message\ntemplates:\n- name: trivy-raw-json\n  rego-package: postee.rawmessage.json\n\n# Actions are target services that should consume the messages\nactions:\n- name: alert-devops\n  type: pagerduty\n  enable: true\n  pagerduty-auth-token: \"<auth token>\"\n  pagerduty-routing-key: \"<routing key>\"\n```"
  },
  {
    "path": "docs/blueprints/external-healthcheck.md",
    "content": "# Distributed Service Healthcheck\n\n## Introduction\nIn this walkthrough, we will setup a globally distributed healthcheck for a service that we expect to be accessible from anywhere. This walkthrough will combine Postee Actions and AWS Lambda to accomplish this.\n\n## Scenario\nA DevOps operator gets paged about a service that they maintain. This page turns out to be flaky and non-actionable. \n\nIn order to avoid operator fatigue, we can confirm the correctness of the page by triggering several healthchecks upon the notification of such an event. These healthchecks are performed externally via Lambda functions spread across different regions. \n\nBy performing such globally distributed checks, the operator can be rest assured of the accuracy of any failures and in addition narrow out the scope of the problem.\n\n![img.png](assets/distributed-healthcheck.png)\n\n## Sample Configs\nIn this case a sample configuration for the components can be described as follows:\n\n### Postee Config\n\nPostee Actions dispatches calls via the HTTP Action to 3 different AWS Lambda URLs. These requests are performed in parallel. In addition, the operator is performed of the trigger and notified via a Slack message.\n\n```yaml\nroutes:\n- name: actions-route\n  input: contains(input.ServiceURL.Reachable, \"false\")\n  actions: [send-slack-message, eu-check, apac-check, na-check]\n\n# Outputs are target services that should consume the messages\nactions:\n- name: send-slack-messsage\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/TAAAA/BBB/<key>\n\n- name: eu-check\n  type: http\n  enable: true\n  url: \"https://<uuid-func>.lambda-url.<aws-region>.on.aws/\"\n  method: POST\n\n- name: apac-check\n  type: http\n  enable: true\n  url: \"https://<uuid-func>.lambda-url.<aws-region>.on.aws/\"\n  method: POST\n\n- name: na-check\n  type: http\n  enable: true\n  url: \"https://<uuid-func>.lambda-url.<aws-region>.on.aws/\"\n  method: POST\n```\n\n### Lambda Config\n```python\nimport os\nfrom datetime import datetime\nfrom urllib.request import Request, urlopen\n\nSITE = \"https://www.my-highly-available-website.com\"\nEXPECTED = \"Text I expect to find\"\n\ndef validate(res):\n    return EXPECTED in res\n\n\ndef lambda_handler(event, context):\n    timeNow = datetime.today().strftime('%Y-%m-%d-%H:%M:%S')\n    print('Checking {} at {}...'.format(SITE, timeNow))\n    try:\n        req = Request(SITE, headers={'User-Agent': 'AWS Lambda'})\n        if not validate(str(urlopen(req).read())):\n            raise Exception('Validation failed')\n    except:\n        print('Check failed!')\n        raise\n    else:\n        print('Check passed!')\n        return ('aquasec.com is up! at {}'.format(timeNow))\n    finally:\n        print('Check complete at {}'.format(str(datetime.now())))\n```\n\nInspired by: https://github.com/amazon-archives/serverless-app-examples/blob/master/python/lambda-canary/lambda_function.py \n"
  },
  {
    "path": "docs/blueprints/image-processing.md",
    "content": "# Doing Serverless Image Recognition using Postee Actions and AWS\n\n## Introduction\nIn this walkthrough, we will setup Postee Actions by re-using existing AWS Lambda Functions. This will allow anyone to configure and re-use existing AWS Lambda functionality as a Postee Action.\n\n## Scenario\nA user wants to be able to setup image processing of captured images in order to be able to identify any suspicious activity captured by the security (CCTV) camera.\n\nUpon successful identification, the user should be informed of such an event.\n\n![img.png](assets/image-rekognition.png)\n\n## Sample Configs\nIn this case a sample configuration for the components can be described as follows:\n\n### Postee Config\n```yaml\nroutes:\n- name: actions-route\n  input: contains(input.Camera.Event, \"Finding\")\n  serialize-actions: true\n  actions: [send-slack-message, process-image]\n\n# Outputs are target services that should consume the messages\nactions:\n- name: send-slack-messsage\n  type: slack\n  enable: true \n  url: https://hooks.slack.com/services/TAAAA/BBB/<key>\n\n- name: process-image\n  type: http\n  enable: true\n  url: \"https://<uuid-func>.lambda-url.<aws-region>.on.aws/\"\n  method: POST\n```\n\n### AWS Rekognition & Lambda Config\nThe full source code is omitted here for brevity but this example was inspired by: https://docs.aws.amazon.com/code-samples/latest/catalog/python-rekognition-rekognition_video_detection.py.html\n\nIn order to setup the Lambda function we will need a handler that can process the incoming event from Postee. The below example demonstrates via Python psuedocode what this Lambda Handler could look like.\n```python\nimport boto3\nfrom rekognition_objects import (\n    RekognitionFace, RekognitionVideo\n)\n\ndef do_face_detection(self):\n    return self._do_rekognition_job(\n        \"face detection\",\n        self.rekognition_client.start_face_detection,\n        self.rekognition_client.get_face_detection,\n        lambda response: [\n            RekognitionFace(face['Face'], face['Timestamp']) for face in\n            response['Faces']])\n\ndef lambda_handler(event, context):\n    rekognition_client = boto3.client('rekognition')\n    video = RekognitionVideo.from_event(event, rekognition_client)\n    faces = video.do_face_detection()\n    return faces\n```"
  },
  {
    "path": "docs/blueprints/trivy-aws-security-hub.md",
    "content": "# Trivy AWS CSPM Scanning\n\n## Introduction\nIn this walkthrough, we will setup AWS Cloud Scanning with [Trivy](https://github.com/aquasecurity/trivy) and send the results to Postee, which in turn will send the results to [AWS Security Hub](https://aws.amazon.com/security-hub/), a CSPM product by AWS.\n\n## Scenario\nA DevOps team would like to configure alerts for their Cloud Security Posture in order to know if they are following the best security practices. This is especially important in those scenarios where compliance can fall out of place during active usage. For this they decide to install Trivy, and use the [AWS Scanning feature](https://www.youtube.com/watch?v=XGfr-9CawV0) to send the results to Postee.\n\nThey decide to configure Postee so that upon receiving such alerts, Postee can action upon them as desired but also report them upstream to the AWS Security Hub for further analysis and triage.\n\n![img.png](assets/trivy-aws-postee.png)\n\n## Sample Configs\nIn this case a sample configuration for the components can be described as follows:\n\n### Postee Config\n\nPostee Actions dispatches calls via the HTTP Action to 3 different AWS Lambda URLs. These requests are performed in parallel. In addition, the operator is performed of the trigger and notified via a Slack message.\n\n```yaml\nactions:\n- type: awssecurityhub\n  enable: true\n  name: Send Findings to Security Hub\nroutes:\n- name: Send Trivy Findings to AWS Security Hub\n  template: raw-json\n  actions:\n  - Send Findings to Security Hub\n  input-files:\n  - Trivy AWS Findings\ntemplates:\n- name: raw-json\n  rego-package: postee.rawmessage.json\nrules:\n- name: Trivy AWS Findings\nname: Send Trivy Results to AWS Security Hub\n```\n\n!!! note\n    Currently Postee AWS Security Hub configuration only supports reading AWS Credentials from the AWS config file present on disk.\n\n### AWS Security Hub configuration\nAWS Security Hub can be configured using the instructions as defined [here](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-settingup.html)\n\n!!! tip\n    AWS Security Hub only accepts Trivy findings from the AWS account that is associated with the findings. The identifier of the associated account is the value of the AwsAccountId attribute for the finding.\n\n### Trivy Webhook Plugin\n[Trivy Webhook Plugin](https://github.com/aquasecurity/trivy-plugin-webhook) is a Trivy plugin that lets you send Trivy scan results to a webhook listening on an endpoint. In this case we can make use of it as follows:\n\n#### Install the plugin\n```shell\ntrivy plugin install github.com/aquasecurity/trivy-plugin-webhook\n```\n\n#### Run the Trivy scan using the plugin\n```shell\ntrivy webhook -- --url=<postee-endpoint> -- <trivy args>\n```"
  },
  {
    "path": "docs/blueprints/trivy-operator.md",
    "content": "# Trivy Operator \n\n## Introduction\nIn this walk through, configure [Trivy Operator](https://github.com/aquasecurity/trivy-operator), a Kubernetes native security toolkit that helps security practitioners detect vulnerabilities, secrets and other misconfigurations in their Kubernetes clusters. We will configure Trivy Operator to send the generated reports to Postee, whereby Postee can take necessary actions on the incoming reports for example, removing vulnerable images.\n\n## Scenario\nA DevOps team would like to configure alerts for their Kubernetes cluster to observe any security vulnerabilities or secrets getting exposed during deployments. This is especially important in those scenarios where compliance can fall out of place during active usage. For this they decide to install Trivy Operator, and use the [Webhook integration](https://aquasecurity.github.io/trivy-operator/latest/integrations/webhook/) to send the reports to Postee.\n\nThey decide to configure Postee so that upon receiving such reports, Postee can action upon them as desired, which could include taking actions such as sending alerts to operators, creating JIRA tickets etc.\n\n![img.png](assets/trivy-operator-webhook.png)\n\n## Sample Configs\nIn this case a sample configuration for the components can be described as follows:\n\n### Postee Config\n\n```yaml\nroutes:\n- name: Trivy Operator Alerts\n  input: input.report.summary.criticalCount > 0 # You can customize this based on your needs\n  actions: [send-slack-msg]\n  template: trivy-operator-slack\n\n# Templates are used to format a message\ntemplates:\n- name: trivy-operator-slack\n  rego-package: postee.trivyoperator.slack\n\n# Actions are target services that should consume the messages\nactions:\n- name: send-slack-msg\n  type: slack\n  enable: true\n  url: <slack webhook url>\n```\n\nIf all goes well, you should see a report in your Slack channel next time it is generated.\n![img.png](assets/trivy-operator-slack-report.png)"
  },
  {
    "path": "docs/blueprints/trivy-vulnerability-scan.md",
    "content": "# Trivy Vulnerability Scan\n\n## Introduction\nIn this walkthrough, we will setup vulnerability scanning with [Trivy](https://github.com/aquasecurity/trivy) and send the results to Postee for creation of JIRA tickets as an example.\n\nA video format of this guide is also available [here](https://youtu.be/HZ5Z8jAVH8w?t=420).\n\n## Scenario\nA DevOps team would like to configure alerts for scheduled vulnerability scans to notify them about any vulnerable images that they might be running in their clusters. For this they decide to install Trivy, run it on a schedule and send the results to Postee.\n\nThey decide to configure Postee so that upon receiving such alerts, Postee creates a JIRA ticket for them to take a look at it at their disposal.\n\n![img.png](../img/trivy-postee.png)\n\n## Sample Configs\nIn this case a sample configuration for the components can be described as follows:\n\n### Postee Config\n```yaml\nroutes:\n- name: trivy-alpine-vulns\n  input: contains(input.Metadata.OS.Family, \"alpine\")\n  actions: [my-jira]\n  template: trivy-raw-json\n\n# Templates are used to format a message\ntemplates:\n- name: trivy-raw-json\n  rego-package: postee.rawmessage.json\n\n# Actions are target services that should consume the messages\nactions:\n- name: my-jira\n  type: jira\n  enable: true\n  url: \"https://foo.bar.com\"\n  user: \"jdoe@foo.bar.com\"\n  password: \"hunter2\"\n  project-key:   \"ABC\"\n  board:     \"Backlog\"\n  labels:    [\"trivy-vulns\"]\n```\n\n### Trivy Webhook Plugin\n[Trivy Webhook Plugin](https://github.com/aquasecurity/trivy-plugin-webhook) is a Trivy plugin that lets you send Trivy scan results to a webhook listening on an endpoint. In this case we can make use of it as follows:\n\n#### Install the plugin\n```shell\ntrivy plugin install https://github.com/aquasecurity/trivy-plugin-webhook\n```\n\n#### Run the Trivy scan using the plugin\n```shell\ntrivy webhook -- --url=<postee-endpoint> -- <trivy args>\n```"
  },
  {
    "path": "docs/config.md",
    "content": "When Postee receives a message it will process it based on routing rules and send it to the appropriate target. How does it know how to do that? Well, this information is defined in Postee's configuration file, [cfg.yaml](https://github.com/aquasecurity/postee/blob/main/cfg.yaml), which contains the following definitions:\n\n1. [General settings](/postee/settings)\n2. [Routes](/postee/routes)\n3. [Templates](/postee/templates)\n4. [Actions](/postee/actions)\n\nThese sections will be described in detail as we proceed through the documentation."
  },
  {
    "path": "docs/controller-runner.md",
    "content": "# Controller Runner Mode\n\n## Introduction\nPostee can also be run in Controller/Runner mode. The idea is to decouple enforcement from execution, if applicable.\n\n## Scenario\nIn the following scenario, consider two services: A and B. In the case of Service A, a Trivy scan is run and results of the scan result are sent to Postee for executing Actions upon.\n\nIn the case of Service B, a Tracee container is constantly monitoring for malicious activity that happens on the host. When a Tracee finding is observed, it is sent to a local Postee Runner. This Postee Runner has the ability to locally execute a pre-defined Postee Action.\n\n![img.png](img/controller-runner.png)\n\n## Configuration\n### Run Postee in Controller mode:\n```shell\npostee --cfgfile=./cfg-controller-runner.yaml --controller-mode --controller-ca-root=\"./rootCA.pem\" --controller-tls-cert=\"./server-cert.pem\" --controller-tls-key=\"./server-key.pem\" --controller-seed-file=\"./seed.txt\"\n```\n\n| Option               | Required                     | Description                            |\n|----------------------|------------------------------|----------------------------------------|\n| controller-mode      | true                         | Enable Postee to run as a Controller   |\n| controller-ca-root   | false                        | TLS CA Root Certificate for Controller |\n| controller-tls-cert  | false                        | TLS Certificate for Controller         |\n| controller-tls-key   | false | TLS Key for Controller                 |\n| controller-seed-file | false | Seed file for Controller               |\n\n??? note \"Example Controller/Runner Configuration\"\n    ```yaml\n    name: Postee Controller Runner Demo\n\n    routes:\n    - name: controller-only-route\n      input: contains(input.image, \"alpine\")\n      actions: [my-http-post-from-controller]\n      template: raw-json\n\n    - name: runner-only-route\n      input: contains(input.SigMetadata.ID, \"TRC-1\")\n      serialize-actions: true\n      actions: [my-exec-from-runner, my-http-post-from-runner]\n      template: raw-json\n\n    - name: controller-runner-route\n      input: contains(input.SigMetadata.ID, \"TRC-2\")\n      actions: [my-exec-from-runner, my-http-post-from-runner, my-http-post-from-controller]\n      template: raw-json\n\n    templates:\n    - name: raw-json\n      rego-package: postee.rawmessage.json\n\n    actions:\n    - name: stdout\n      type: stdout\n      enable: true\n\n    - name: my-http-post-from-controller\n      type: http\n      enable: true\n      url: \"https://webhook.site/<uuid>\"\n      method: POST\n      headers:\n        \"Foo\": [ \"bar\" ]\n      timeout: 10s\n      body-content: |\n        This is an example of a inline body\n        Input Image: event.input.image\n\n    - name: my-exec-from-runner\n      runs-on: \"postee-runner-1\"\n      type: exec\n      enable: true\n      env: [\"MY_ENV_VAR=foo_bar_baz\", \"MY_KEY=secret\"]\n      exec-script: |\n        #!/bin/sh\n        echo $POSTEE_EVENT\n        echo \"this is hello from postee\"\n\n    - name: my-http-post-from-runner\n      runs-on: \"postee-runner-1\"\n      type: http\n      enable: true\n      url: \"https://webhook.site/<uuid>\"\n      method: POST\n      body-content: |\n        This is an another example of a inline body\n        Event ID: event.input.SigMetadata.ID\n    ```\n\nThe only notable change in the configuration as defined is of the Actions that can run on Runners. Observe the `runs-on` clause below.\n```yaml\n- name: my-exec-from-runner\n  runs-on: \"postee-runner-1\"\n  type: exec\n  enable: true\n  exec-script: |\n    #!/bin/sh\n    echo $POSTEE_EVENT\n    echo \"this is hello from postee\"\n```\n\nIn this case this particular Action will run on Postee Runner that identifies itself as `postee-runner-1`\n\n### Run Postee in Runner mode:\n```shell\npostee --controller-url=\"nats://0.0.0.0:4222\" --runner-ca-cert=\"./rootCA.pem\" --runner-tls-cert=\"./runner-cert.pem\" --runner-tls-key=\"./runner-key.pem\" --runner-seed-file=\"./seed.txt\", --runner-name=\"postee-runner-1\"  --url=0.0.0.0:9082 --tls=0.0.0.0:9445\n```\n\n| Option           | Required                 | Description                                              |\n|------------------|--------------------------|----------------------------------------------------------|\n| controller-url   | true                     | The URL to the Postee Controller                         |\n| runner-name      | true                     | The Name of the Runner, as defined in configuration YAML |\n| runner-ca-root   | false                    | TLS Root CA Certificate for Runner                       |\n| runner-tls-cert  | false                    | TLS Certificate for Runner                               |\n| runner-tls-key   | false | TLS Key for Runner                                       |\n| runner-seed-file | false | Seed file for Runner                                     |\n\n\n### Secured Controller/Runner Channel\nThe communication channel between Controller and Runner can be optionally secured with TLS and be Authentication (AuthN). \n\nTLS can be enabled by passing the TLS cert and key through the optional `--controller-tls-cert` and `--controller-tls-key` flags for Controller and `--runner-tls-cert` and `--runner-tls-key` flags for Runner.\n\nAuthN can be enabled by passing the [NATS Seed File](https://docs.nats.io/running-a-nats-service/configuration/securing_nats/auth_intro/nkey_auth). Postee uses NKeys, a public-key signature system based on Ed25519. \n\nA seed file should be treated as a secret. It can be passed to the Controller via the `--controller-seed-file` and the Runner via `--runner-seed-file`.\n\nThis can be helpful in situations where Postee Config contains secrets that are configured in an Action that runs on a Runner. \n\n## Walkthrough\nIn the case of Tracee reporting a malicious finding, the Action might only make sense to run locally within the same environment where Tracee reported from. For instance, in the case of a Postee Action to kill a process reported within the malicious finding, the process will only exist on the host where Tracee reported from. Therefore, the need for a localized Postee that can handle this arises.\n\nPostee Runners can automatically bootstrap themselves upon startup, given the address of the Postee Controller. They only receive the relevant config info from the Postee Controller for the Actions and Routes they are responsible for. This helps by limiting the spread of secrets in your configuration to only those Runners where they are needed. If your deployment uses Actions where secrets are required, we recommend you run these Actions at the Controller level.\n\nThe only Actions that a Postee Runner should run are Actions that are context/environment specific. A few examples (but not limited to) are: Killing a local process, Shipping local logs on host to a remote endpoint, etc.\n\n## Additional Info\nPostee Runners and Controllers are no different from a normal instance of vanilla Postee. Therefore, no changes to the producers are required to use this functionality.\n\nAll events received by Postee Runners are reported upstream to the Controller. This has two benefits:\n\n1. Executions and Events received by the Runners can be monitored at a central level (Controller).\n2. Mixing of Runner and Controller Actions within a single Route, for ease of usage.\n\nMixing of Runner and Controller Actions can be explained with a following sample configuration:\n```yaml\n- name: controller-only-route\n  input: contains(input.image, \"alpine\")\n  actions: [my-slack-message-from-controller]\n  template: raw-json\n\n- name: runner-only-route\n  input: contains(input.SigMetadata.ID, \"TRC-1\")\n  serialize-actions: true\n  actions: [my-exec-from-runner, my-http-post-from-runner]\n  template: raw-json\n\n- name: controller-runner-route\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  serialize-actions: true\n  actions: [my-exec-from-runner, my-http-post-from-runner, my-jira-ticket-from-controller]\n  template: raw-json\n```\n\nIn this sample configuration, we have three routes. One that solely executes on the Controller, another that solely executes on the Runner and a Mixed route.\n\nIn the case of the Mixed route, the first two Actions are run on the Runner. These Actions are run locally as they might require environment specific things to run, as discussed above. The third Action is run from a Controller because of security reasons to not distribute secrets to a Runner. \n\n#### A quick note on Serialization\nThe option of `serialize-actions` works as expected and guarantees true serialization for execution of Actions in the case of Controller only and Runner only routes. But for the case of Mixed routes (as described above) where executions can run on both Controller and Runner, this serialization cannot be strongly guaranteed due to the difference of execution environments (Runner and Controller).\n"
  },
  {
    "path": "docs/demo.md",
    "content": "In this demo, we’ll walk through a scenario in which a user wants to act on a security event received from Tracee, an open source runtime security tool. In this scenario, the user will set up the Postee Exec Action to save logs for forensic purposes and then use the Postee HTTP Action to ship the saved logs to a remote server.\n\nIn this case, the incoming security event from Tracee is received by Postee and evaluated by the following route YAML definition:\n\n![img.png](img/img.png)\n\nAs seen above, the route has a Rego rule that evaluates the input to contain a certain signature ID, TRC-2, which represents anti-debugging activity. In addition, if the input is matched, the output is triggered.\n\n## Exec Action\n\nIn this case, we call the Exec Action first and then the HTTP Action. They are defined as the following:\n\nThe Exec Action can take in the following parameters:\n\n| Option      | Usage                                                                                     |\n|-------------|-------------------------------------------------------------------------------------------|\n| env         | Optional, custom environment variables to be exposed in the shell of the executing script |\n| input-file  | Required, custom shell script to executed                                                 |\n| exec-script | Required, inline shell script executed                                                    |\n\nThe Exec Action also internally exposes the `$POSTEE_EVENT` environment variable with the input event that triggered the action. This can be helpful in situations where the event itself contains useful information.\n\nBelow is an example of using `$POSTEE_EVENT`. It uses the inline exec-script script:\n\n![img_3.png](img/img_3.png)\n\nAs you can see, we capture the incoming Postee event and write this event to the Tracee event log for forensic purposes.\n\n## HTTP Action\n\nFinally, we can configure the Postee HTTP Post Action to ship the captured event logs via our HTTP Action to our remote server.\n\n![img_1.png](img/img_1.png)\n\n| Option   | Usage                                   |\n|----------|-----------------------------------------|\n| URL      | Required, URL of the remote server      |\n| Method   | Required, e.g., GET, POST               |\n| Headers  | Optional, custom headers to send        |\n| Timeout  | Optional, custom timeout for HTTP call  |\n| Bodyfile | Optional, input file for HTTP post body |\n\nTo run Postee in the container, we can invoke the Postee Docker container:\n\n```\ndocker run --rm --name=postee \\\n-v <path-to-cfg>:/config/cfg-actions.yaml  \\\n-e POSTEE_CFG=/config/cfg-actions.yaml \\\n-e POSTEE_HTTP=0.0.0.0:8084  \\\n-e POSTEE_HTTPS=0.0.0.0:8444  \\\n-p 8084:8084 -p 8444:8444 aquasecurity/postee:latest\n```\n\n"
  },
  {
    "path": "docs/deployment.md",
    "content": "# Deployment\n\n## Kubernetes\n\nDue to a limitation in how persistent volumes are handled in EKS, we have to ensure that both components sharing DB and CFG volumes are deployed to the same physical K8s node. This can be achieved by setting a `podAffinity` in the `values.yaml` file.\n\n```yaml\n# BUG: postee-0 und posteeui both need access to the same PVC (database) so we need to ensure both run on the same node\naffinity:\n  podAffinity:\n    requiredDuringSchedulingIgnoredDuringExecution:\n    - labelSelector:\n        matchExpressions:\n        - key: app.kubernetes.io/instance\n          operator: In\n          values:\n          - postee\n      topologyKey: kubernetes.io/hostname\n```\n"
  },
  {
    "path": "docs/examples.md",
    "content": "Here are some Postee configuration samples to showcase a variety of use cases.\n\n??? example \"Forward all \"Block\" audit events\"\n    ```yaml\n    name: myserver\n    aqua-server: https://myserver.com\n    max-db-size: 1000MB\n    delete-old-data: 100\n    db-verify-interval: 1\n\n    routes:\n    - name: team-drift\n      input: input.level = \"block\"\n      actions: [my-teams]\n      template: raw-html\n\n    actions:\n    - name: my-teams\n      type: teams\n      enable: true\n      url: https://outlook.office.com/webhook/<replace>\n\n    templates:\n    - name: raw-html\n      rego-package:  postee.rawmessage.html\n    ```\n\n??? example \"Forward Critical vulnerabilities\"\n    ```yaml\n    # This example will forward events of images with critical vulnerabilities to MS Teams.\n    # Note that duplicate events of same image will be ignored for 30 days.\n\n    name: myserver\n    aqua-server: https://myserver.com\n    max-db-size: 1000MB\n    delete-old-data: 100\n    db-verify-interval: 1\n\n    routes:\n    - name: team-critical-vul\n      input: input.vulnerability_summary.critical > 0\n      actions: [my-teams]\n      template: raw-html\n      plugins:\n      unique-message-props: [\"digest\",\"image\",\"registry\", \"vulnerability_summary.high\", \"vulnerability_summary.medium\", \"vulnerability_summary_low\"]\n      unique-message-timeout: 30d\n\n    actions:\n    - name: my-teams\n      type: teams\n      enable: true\n      url: https://outlook.office.com/webhook/<replace>\n\n    templates:\n    - name: raw-html\n      rego-package:  postee.rawmessage.html\n    ```\n\n??? example \"Forward Drift events\"\n    ```yaml\n    # This example will forward events of Drift Prevention to MS Teams.\n\n    name: myserver\n    aqua-server: https://myserver.com\n    max-db-size: 1000MB       #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\n    delete-old-data: 100    # delete data older than N day(s).  If empty then we do not delete.\n    db-verify-interval: 1   # hours. an Interval between tests of DB. Default: 1 hour\n\n    routes:\n    - name: team-drift\n      input: contains(input.control, \"Drift\")\n      actions: [my-teams]\n      template: raw-html\n\n    actions:\n    - name: my-teams\n      type: teams\n      enable: true\n      url: https://outlook.office.com/webhook/<replace>\n\n    templates:\n    - name: raw-html                        #  Raw message json\n      rego-package:  postee.rawmessage.html #  HTLM template REGO package\n    ```\n\n??? example \"Add Kubernetes Labels and Annotations\"\n    ```yaml\n    name: tenant\n    aqua-server:\n    max-db-size: 1000MB\n    db-verify-interval: 1\n\n    routes:\n    - name: stdout\n      actions: [ stdout ]\n      template: raw-json\n\n    - name: actions-route\n      input: contains(input.SigMetadata.ID, \"TRC-2\")\n      actions: [my-k8s]\n      template: raw-json\n\n    templates:\n    - name: raw-json\n      rego-package: postee.rawmessage.json\n\n    actions:\n    - name: stdout\n      type: stdout\n      enable: true\n\n    - name: my-k8s\n      type: kubernetes\n      enable: true\n      kube-namespace: \"default\"\n      kube-config-file: \"/path/to/kubeconfig\"\n      kube-label-selector: \"app=nginx-app\"\n      kube-actions:\n      labels:\n      foo-label: \"bar-value\"\n      bar-label: event.input.SigMetadata.ID\n      annotations:\n      foo-annotation: \"bar-value\"\n      bar-annotation: event.input.SigMetadata.ID\n    ```\n\n??? example \"Run ad-hoc docker image\"\n    ```yaml\n    name: tenant\n    aqua-server:\n    max-db-size: 1000MB\n    db-verify-interval: 1\n\n    routes:\n    - name: stdout\n      actions: [ stdout ]\n      template: raw-json\n\n    - name: actions-route\n      input: contains(input.SigMetadata.ID, \"TRC-2\")\n      actions: [stop-vulnerable-pod]\n      template: raw-json\n\n    templates:\n    - name: raw-json\n      rego-package: postee.rawmessage.json\n\n    actions:\n    - name: stdout\n      type: stdout\n      enable: true\n\n    - name: stop-vulnerable-pod\n      type: docker\n      enable: true\n      docker-image-name: \"bitnami/kubectl:latest\"                          \n      docker-cmd: [\"delete\", \"pod\", event.input.SigMetadata.hostname]\n      docker-network: \"host\"\n      docker-volume-mounts:\n      \"path/to/.kube/config\": \"/.kube/config\"\n    ```\n\n??? example \"Collect and send logs\"\n    ```yaml\n    name: tenant\n    aqua-server: localhost\n    max-db-size: 1000MB\n    db-verify-interval: 1\n\n    routes:\n    - name: stdout\n      actions: [ stdout ]\n      template: raw-json\n\n    - name: actions-route\n      input: contains(input.SigMetadata.ID, \"TRC-2\")\n      serialize-actions: true\n      actions: [my-exec, my-http-post-file, my-http-post-content]\n      template: raw-json\n\n    templates:\n    - name: raw-json\n      rego-package: postee.rawmessage.json\n\n    actions:\n    - name: stdout\n      type: stdout\n      enable: true\n\n    - name: my-exec\n      type: exec\n      enable: true\n      env: [\"MY_ENV_VAR=foo_bar_baz\", \"MY_KEY=secret\"]\n      exec-script: |\n      #!/bin/sh\n      echo $POSTEE_EVENT >> /tmp/postee.event.logs\n\n    - name: my-http-post-file\n      type: http\n      enable: true\n      url: \"https://my-fancy-url.com\"\n      method: POST\n      body-file: /tmp/postee.event.logs\n\n    - name: my-http-post-content\n      type: http\n      enable: true\n      url: \"https://my-fancy-url.com\"\n      method: POST\n      headers:\n      \"Foo\": [ \"bar\" ]\n      \"Haz\": [ \"baz\" ]\n      timeout: 10s\n      body-content: |\n      This is an example of a inline body\n      Event ID: event.input.Signature.ID   \n    ```"
  },
  {
    "path": "docs/improvements.md",
    "content": "# Improvements\n\nPostee like any other software isn't perfect and as the writing of this document can be improved in the following areas:\n\n## UI\n\nThis is an improvement that would benefit not just new users of Postee but also add ease of use for existing users to configure Postee on the fly with drag and drop-'ing of components to configure Postee.\n\n![img.png](img/postee-ui-drag-and-drop.png)\n\nThe above is an example of a User Interface that Postee could have where the blocks (Trivy, AWS Security Hub and Slack) are dragged and dropped into the view and connected as needed. This would translate into a Postee configuration file being written to disk.\n\n\n## Alternate Policy language\n\nToday Postee supports Rego as the primary language for policy evaluation. While Rego is purposely suited for being a policy language, it might be challenging to learn for new users and feel comfortable in. \n\nTherefore, having an alternate policy language to write rules could benefit with Postee adoption even further. A few ideas that we've experimented in some of our other projects are as follows:\n\n1. Golang Policies\n2. [CEL-Go](https://github.com/google/cel-go)\n\n## Support for more Actions\n\nToday Postee supports a wide variety of Postee Actions but the list can be further expanded by including the following:\n\n1. AWS Cloudwatch Logs\n2. Azure automation\n3. GCP automation\n\nExtending Postee to support a new Action is very simple. You can take a look at this PR to see exactly which places you'll need to modify in order to support a new Action. \n\n[Link to example PR](https://github.com/aquasecurity/postee/pull/468)\n\n\n## My idea is not listed here\nDo you have an idea that you'd like to implement in Postee? Reach out to us via GitHub Issues or on Slack to discuss more about it.\n"
  },
  {
    "path": "docs/index.md",
    "content": "#\n\n<figure markdown>\n  ![Postee Logo](img/postee.png){ align=\"center\" }\n</figure>\n\n\nPostee is a simple message routing application that receives input messages through a webhook interface, and can take enforce actions using predefined outputs via integrations.\n\nPrimary use of Postee is to act as a message relay and notification service that integrates with a variety of third-party services. Postee can also be used for sending vulnerability scan results or audit alerts from Aqua Platform to collaboration systems.\n\nIn addition, Postee can also be used to enforce pre-defined behaviours that can orchestrate actions based on input messages as triggers.\n\n![Postee v2 scheme](img/postee-v2-scheme.png)\n"
  },
  {
    "path": "docs/install.md",
    "content": "To run Postee you will first need to configure the [Postee Configuration File](/postee/config), which contains all the message routing logic.\nAfter the configuration file is ready, you can run the official Postee container image: **aquasec/postee:latest**, or compile it from source.\n\nThere are different options to mount your customize configuration file to Postee - if running as a Docker container, then you simply mount the configuration files as a volume mount. If running as a Kubernetes deployment, you will need to mount it as a ConfigMap. See the below usage examples for how to run Postee on different scenarios.\n\nAfter Postee will run, it will expose two endpoints, HTTP and HTTPS. You can send your JSON messages to these endpoints, where they will be delivered to their target system based on the defined rules.\n\n### Docker\nTo run Postee as a Docker container, you mount the cfg.yaml to '/config/cfg.yaml' path in the Postee container.\n\n\n```bash\ndocker run -d --name=postee -v /<path to configuration file>/cfg.yaml:/config/cfg.yaml \\\n    -e POSTEE_CFG=/config/cfg.yaml -e POSTEE_HTTP=0.0.0.0:8084 -e POSTEE_HTTPS=0.0.0.0:8444 \\\n    -p 8084:8084 -p 8444:8444 aquasec/postee:latest\n```\n\n### Kubernetes\nWhen running Postee on Kubernetes, the configuration file is passed as a ConfigMap that is mounted to the Postee pod.\n\n\n#### Cloud Providers\n\n``` bash\nkubectl create -f https://raw.githubusercontent.com/aquasecurity/postee/main/deploy/kubernetes/postee.yaml\n```\n\n#### Using HostPath\n\n``` bash\nkubectl create -f https://raw.githubusercontent.com/aquasecurity/postee/main/deploy/kubernetes/hostPath/postee-pv.yaml\n```\n\n!!! Note \"Persistent Volumes Explained\"\n    - `postee-db`: persistent storage directory `/server/database`\n    - `postee-config`: mount the cfg.yaml to a writable directory `/config/cfg.yaml`\n    - `postee-rego-templates`: mount custom rego templates\n    - `postee-rego-filters`: mount custom rego filters\nTo edit the default Postee-UI user\n\n```\nkubectl -n postee set env deployment/my-posteeui -e POSTEE_ADMIN_USER=testabc -e POSTEE_ADMIN_PASSWORD=password\n```\n\nThe Postee endpoints\n```\nhttp://postee-svc.default.svc.cluster.local:8082\n```\n```\nhttps://postee-svc.default.svc.cluster.local:8445\n```\n\nThe Postee-UI endpoint\n````\nhttp://postee-ui-svc.default.svc.cluster.local:8000\n````\n\n#### Controller/Runner\nTo use Controller/Runner functionality within Kubernetes, you can follow a reference manifest implementation:\n- [Controller](https://github.com/aquasecurity/postee/blob/main/deploy/kubernetes/postee-controller.yaml)\n- [Runner](https://github.com/aquasecurity/postee/blob/main/deploy/kubernetes/postee-runner.yaml)\n\n### Helm\nWhen running Postee on Kubernetes, the configuration file is passed as a ConfigMap that is mounted to the Postee pod.\n\nThis chart bootstraps a Postee deployment on a [Kubernetes](https://kubernetes.io/) cluster using the [Helm package manager](https://helm.sh/).\n\n#### Prerequisites\n- Kubernetes 1.17+\n- Helm 3+\n\n#### Test the Chart Repository\n\n```bash\ncd deploy/helm\nhelm install my-postee -n postee --dry-run --set-file applicationConfigPath=\"../../cfg.yaml\" ./postee\n```\n\n#### Installing the Chart from the Source Code\n\n```bash\ncd deploy/helm\nhelm install app --create-namespace -n postee ./postee\n```\n\n#### Installing from the the Aqua Chart Repository\n\nLet's add the Helm chart and deploy Postee executing:\n\n\n```bash\nhelm repo add aquasecurity https://aquasecurity.github.io/helm-charts/\nhelm repo update\nhelm search repo postee\nhelm install app --create-namespace -n postee aquasecurity/postee\n```\n\nCheck that all the pods are in Running state:\n\n`kubectl get pods -n postee`\n\nWe check the logs:\n\n```\nkubectl logs deployment/my-posteeui -n postee | head\n```\n\n```\nkubectl logs statefulsets/my-postee -n postee | head\n```\n\n#### Delete Chart\n\n```bash\nhelm -n postee delete my-postee\n```\n\n#### From Source\nClone and build the project:\n```bash\ngit clone git@github.com:aquasecurity/postee.git\nmake build\n```\nAfter that, modify the cfg.yaml file and set the 'POSTEE_CFG' environment variable to point to it.\n```bash\nexport POSTEE_CFG=<path to cfg.yaml>\n./bin/postee\n```\n"
  },
  {
    "path": "docs/routes.md",
    "content": "A route is used to control message flows. Each route includes the input message condition, the template that should be used to format the message, and the action(s) that the message should be delivered to.\n\nThe most important part of a route is the **input definition using the Rego language** to define what are the conditions for an incoming message to be handled by a certain route.\n\n![settings](img/postee-email-route.png)\n\n!!! tip\n    See the complete Rego Language in [OPA-reference](https://www.openpolicyagent.org/docs/latest/policy-reference/#built-in-functions)\n\nAfter defining the route's input condition, what is left is to define the template that will be used to format the input message, and the action that formatted message will be sent to.\n\nThe below table describes the fields to define a route:\n\nKey | Description | Possible Values                                       | Example\n--- | --- |-------------------------------------------------------| ---\n*name*|Unique name of route| string                                                | teams-vul-route\n*input*|A Rego rule to match against incoming messages. If there is a match then this route will be chosen for the incoming message| Rego language statements                              | contains(input.message,\"alpine\")\n*input-files*|One or more files with Rego rules| Set of Rego language files                            | [\"Policy-Registry.rego\", \"Policy-Min-Vulnerability.rego\"]\n*actions*|One or more actions that are defined in the \"actions\" section| Set of action names. At least one element is required | [\"my-slack\", \"my-email\"].\n*serialize-actions*|Serialize the list of actions| true, false(default)                                  |true\n*template*| A template that is defined in the \"template\" section| any template name                                     | raw-html\n\nThe `rego-filters` folder contains examples of policy related functions. You can use the examples. To do this, you need to change the input data in the arrays of rego files and fill in the config file. If you want to use an other folder, set the 'REGO_FILTERS_PATH' environment variable to point to it. When using 2 or more files, they will be combined by \"OR\".\nTo combine policy related functions by \"AND\", use the `Policy-Related-Features.rego` file, change the input data, and fill in the required function in allow.\n```\nallow{\n    PermitImageNames\n    PermitMinVulnerability\n}\n```\nIf you are using your own rego files, then the **package** field should be \"postee\" and the result should be in the  **allow** function:\n```\npackage postee\n\nyour_function{...} # 0 or more your functions\n\nallow {\n    your_function\n}\n```\nFor example, the following input definition will match JSON messages that have 'image.name' field with value that contains the string 'alpine':\n\n```\ninput: contains(input.image,\"alpine\")\n```\n\nAnother example using regular expression:\n```\ninput: regex.match(\"alp:*\", input.image)\n```\n\nYou can create more complex input definitions using the Rego language. For example, the following input definition will match JSON messages that have 'image.name' field with value 'alpine' and that their registry is 'Docker Hub' and they have a critical vulnerability.\n\n```\ninput: |\n  contains(input.image,\"alpine\")\n  contains(input.registry, \"Docker Hub\")\n  input.vulnerability_summary.critical>0\n```\n\n## Postee Route Configuration\n\nYou could use Postee with any json. See the following example receiving json events:\n\n### Route All Messages\nTo create a route that matches all messages, simply use the following:\n\n```\nroutes:\n- name: catch-all\n  input: input\n  ...\n```\n\n### Route Drift Prevention Messages\nTo create a route that matches only messages that originated from a \"Drift Prevention\" event, use the following:\n\n```\nroutes:\n- name: catch-drift\n  input: contains(input.control, \"Drift\")\n  ...\n```\n\n### Route Tracee Message\n\nThe following input JSON message is from [Tracee](https://github.com/aquasecurity/tracee).\n\nSet `input` property of route to: `contains(input.SigMetadata.ID,\"TRC-\")` to limit the route to handle Tracee messages only\n\nIn the section [rego-templates](https://github.com/aquasecurity/postee/tree/main/rego-templates) have rego templates samples to use with Tracee:\n- tracee-html.rego\n- tracee-slack.rego\n\n### Plugins\n\n'Plugins' section contains configuration for useful Postee features.\n\nKey | Description | Possible Values | Example\n--- | --- | --- | ---\n*aggregate-message-number*|Number of messages to aggregate into one message.| any integer value | 10\n*aggregate-message-timeout*|number of seconds, minutes, hours to aggregate|Maximum is 24 hours Xs or Xm or Xh | 1h\n*unique-message-props*|Optional. Comma separated list of properties which uniquely identifies an event message. If message with same property values is received more than once, consequitive messages will be ignored. | Array of properties that their value uniquely identifies a message | To avoid duplicate scanning messages you can use the following properties: ```unique-message-props: [\"digest\",\"image\",\"registry\", \"vulnerability_summary.high\", \"vulnerability_summary.medium\", \"vulnerability_summary.low\"]```\n*unique-message-timeout*|Optional. Used along with *unique-message-props*, has no effect if unique props are not specified. Number of seconds/minutes/hours/days before expiring of a message. Expired messages are removed from db. If option is empty message is never deleted | 1d\n"
  },
  {
    "path": "docs/settings.md",
    "content": "General settings are specified at the root level of cfg.yaml. They include general configuration that applies to the Postee application.\n\n![settings](img/postee-settings.png)\n\nKey | Description                                                                                                                                                                          | Possible Values | Example Value\n--- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | ---\n*aqua-server*| Aqua Platform URL. This is used for some of the integrations to will include a link to the Aqua UI                                                                                   | Aqua Platform valid URL | https://server.my.aqua\n*db-verify-interval*| Specify time interval (in hours) for Postee to perform database cleanup jobs. Default: 1 hour                                                                                        | any integer value  | 1\n*max-db-size*| The maximum size of Postee database (in B, KB, MB or GB). Once reached to size limit, Postee will delete old cached messages. If empty then Postee database will have unlimited size | any integer value with a unit siffux | 200kb, 1000 MB, 1Gb\n\n"
  },
  {
    "path": "docs/templates.md",
    "content": "Templates are used to format input messages before sending them to the action. For example - before sending a message to Microsoft Teams there is a need to format the input JSON into an HTML. This is done using a template.\n\nEach template has a `name` field, which is used by the route to assign the template to input and output.\n\n!!! tip\n    Use the default Legacy template \"html\" for general output\n\n![settings](img/postee-template-default.png)\n\nIn addition to name, a template will have **one** of the 4 below keys:\n\nKey | Description | Example\n--- | --- | ---\n*rego-package*|Postee loads bundle of templates from `rego-templates` folder. This folder includes several templates shipped with Postee, which can be used out of the box. You can add additional custom templates by placing Rego file under the 'rego-templates' directory.| `postee.vuls.html`\n*body*| Specify inline template. Relative small templates can be added to config directly | input\n*url*| Load from url. Rego template can be loaded from url.| http://myserver.com/rego.txt\n*legacy-scan-renderer*| Legacy templates are introduced to support Postee V1 renderers. Available values are  \"jira\", \"slack\", \"html\". \"jira\" should be used for jira integration, \"slack\" is for slack and \"html\" is for everything else. | html\n\n!!! tip \n    Pre made examples for templates can be found [here](https://github.com/aquasecurity/postee/tree/main/rego-templates)\n\n### Customizing Templates\nIt is possible to customize templates and even write new ones from scratch. Follow the guide in our advanced section here: [Customizing Templates](advanced.md#Customizing Templates)\n\n### Troubleshooting of Rego Templates\n\nRego templates provide very flexible way for transformation of received json. You can convert received information to html or json.\nOn the flip side sometimes it may be difficult to find root cause of issue (if you run into any while configuring custom template).\nPostee application doesn't have many options to provide detailed error message. Very often if something goes wrong then 'result' property is omitted from rego evaluation result and it causes errors like:\n```\n2021/07/23 18:27:31 Error while evaluating input: property result is not found\n```\nSo here are details to help with troubleshooting:\n#### Required tools\n- [opa](https://www.openpolicyagent.org/docs/latest/#running-opa) - tool to evaluate OPA queries directly\n- [jq](https://stedolan.github.io/jq/) - flexible command-line JSON processor.\n\n#### Evaluate template to build html\nHere is example of command to evaluate rego:\n```\nopa eval data.postee.vuls.html.result --data vuls-html.rego --data common/common.rego --input <path to input json> | jq -r .result[0].expressions[0].value\n```\nThe example above should be started in `rego-templates` folder and evaluates default html template shipped with postee. First opa argument is query. Three parts are used to build query `data`.`<your rego package>`.`result`. You may want to evaluate title property. In this case query would be: `data`.`<your rego package>`.`title`\n\n#### Evaluate template to build json\n\n```\ncd rego-templates\nopa eval data.postee.vuls.slack.result --data vuls-slack.rego --data common/common.rego --input <path to input json> | jq .result[0].expressions[0].value\n```\n\nThe command above is similar to html case but `jq` is used a bit different way.\n"
  },
  {
    "path": "docs/troubleshooting-of-rego-templates.md",
    "content": ""
  },
  {
    "path": "docs/ui.md",
    "content": "Postee provides a simple Web UI to simplify the configuration management.\n\n\n![Config app](img/postee-output-config.png)\n\n## Configure and run Postee UI application\n\n### Requirements\nPostee Admin application shares location of `cfg.yaml` with main webhook app, also Bolt database needs to be in folder which is available for both apps.\n\n!!! danger\n        If application config is submitted by UI app then all yaml comments are removed. So if comments are important please make backup of config yaml.\n\n### Kubernetes for Postee UI application\n\nThe manifest is [here](https://github.com/aquasecurity/postee/blob/main/deploy/kubernetes/postee.yaml).\n\nIt will expose a service `postee-ui-svc` in the port `8000`.\n\n`http://postee-ui.default.svc.cluster.local:8000`\n\n\n### Docker Image for Postee UI application\nDockerfile to build image for UI app is [here](Dockerfile.ui)\n\n### Orchestration example (Docker Compose)\nThere is an example of [docker-compose.yml](docker-compose.yml) that can be used to simplify deploying of both app. Notice that two shared volumes are used. One is for Bolt db and second to store app config. To start apps use: `docker-compose up`.\n\n### Environment variables\nName | Description | Default value\n--- | --- | ---\nPOSTEE_UI_CFG|Path to app config| required, no default value\nPOSTEE_UI_PORT|Port to use with UI app| 8090\nPOSTEE_UI_UPDATE_URL|Url of webhook application|required\nPOSTEE_ADMIN_USER|Admin account name|admin\nPOSTEE_ADMIN_PASSWORD|Admin account password|admin\n"
  },
  {
    "path": "formatting/eval.go",
    "content": "package formatting\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\ntype legacyScnEvaluator struct {\n\tlayoutProvider layout.LayoutProvider\n}\n\nfunc (legacyScnEvaluator *legacyScnEvaluator) Eval(in map[string]interface{}, serverUrl string) (map[string]string, error) {\n\tscan, err := toScanImage(in)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttitle := fmt.Sprintf(\"%s vulnerability scan report\", in[\"image\"])\n\timage_url_part := scan.Registry + \"/\" + url.QueryEscape(scan.Image)\n\n\treturn map[string]string{\n\t\t\"title\":       title,\n\t\t\"description\": layout.GenTicketDescription(legacyScnEvaluator.layoutProvider, scan, nil, serverUrl, image_url_part),\n\t\t\"url\":         serverUrl + image_url_part,\n\t}, nil\n}\nfunc (legacyScnEvaluator *legacyScnEvaluator) IsAggregationSupported() bool {\n\treturn true\n}\n\nfunc (legacyScnEvaluator *legacyScnEvaluator) BuildAggregatedContent(scans []map[string]string) (map[string]string, error) {\n\tvar descr bytes.Buffer\n\tvar urls bytes.Buffer\n\towners := []string{}\n\tfor _, scan := range scans {\n\t\tdescr.WriteString(legacyScnEvaluator.layoutProvider.TitleH1(scan[\"title\"]))\n\t\tdescr.WriteString(scan[\"description\"])\n\t\tif urls.Len() > 0 {\n\t\t\turls.WriteByte('\\n')\n\t\t}\n\t\turls.WriteString(scan[\"url\"])\n\t\tif len(scan[\"owners\"]) > 0 {\n\t\t\towners = append(owners, scan[\"owners\"])\n\t\t}\n\t}\n\ttitle := \"Vulnerability scan report\"\n\n\tr := map[string]string{\n\t\t\"title\":       title,\n\t\t\"description\": descr.String(),\n\t\t\"url\":         urls.String(), //TODO this is strange ...\n\t}\n\n\tif len(owners) > 0 {\n\t\tr[\"owners\"] = strings.Join(owners, \";\")\n\t}\n\treturn r, nil\n}\n\nfunc toScanImage(in map[string]interface{}) (*data.ScanImageInfo, error) {\n\tsource, err := json.Marshal(in) //back to bytes\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tscanInfo := new(data.ScanImageInfo)\n\n\terr = json.Unmarshal(source, scanInfo)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn scanInfo, nil\n}\n\nfunc BuildLegacyScnEvaluator(layoutType string) (data.Inpteval, error) {\n\tswitch layoutType {\n\tcase \"slack\":\n\t\treturn &legacyScnEvaluator{\n\t\t\tlayoutProvider: &SlackMrkdwnProvider{},\n\t\t}, nil\n\tcase \"html\":\n\t\treturn &legacyScnEvaluator{\n\t\t\tlayoutProvider: &HtmlProvider{},\n\t\t}, nil\n\tcase \"jira\":\n\t\treturn &legacyScnEvaluator{\n\t\t\tlayoutProvider: &JiraLayoutProvider{},\n\t\t}, nil\n\tdefault:\n\t\treturn nil, errors.New(\"unknown layout type\")\n\t}\n}\n"
  },
  {
    "path": "formatting/eval_test.go",
    "content": "package formatting\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar (\n\tscan1 = `{\n\t\t\"image\":\"Demo mock image1\",\n\t\t\"registry\":\"registry1\",\n\t\t\"digest\":\"abc\",\n\t\t\"vulnerability_summary\":{\"critical\":0,\"high\":1,\"medium\":3,\"low\":4,\"negligible\":5},\n\t\t\"image_assurance_results\":{\"disallowed\":true}\n\t}`\n)\n\nfunc TestEval(t *testing.T) {\n\texpectedTitle := \"Demo mock image1 vulnerability scan report\"\n\texpectedDescription := `<p>Image name: Demo mock image1</p>\n<p>Registry: registry1</p>\n<p>Image is non-compliant</p>\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n<TR>\n<TH style='padding: 5px;'>CRITICAL</TH><TH style='padding: 5px;'>HIGH</TH><TH style='padding: 5px;'>MEDIUM</TH><TH style='padding: 5px;'>LOW</TH><TH style='padding: 5px;'>NEGLIGIBLE</TH>\n</TR>\n<TR>\n<TD style='padding: 5px;'><span style='color:#c00000'>0</span></TD><TD style='padding: 5px;'><span style='color:#e0443d'>1</span></TD><TD style='padding: 5px;'><span style='color:#f79421'>3</span></TD><TD style='padding: 5px;'><span style='color:#e1c930'>4</span></TD><TD style='padding: 5px;'><span style='color:green'>5</span></TD>\n</TR>\n</TABLE>\n`\n\n\tin := map[string]interface{}{}\n\tif err := json.Unmarshal([]byte(scan1), &in); err != nil {\n\t\tt.Fatalf(\"json.Unmarshal error for %s: %v\\n\", scan1, err)\n\t}\n\te, err := BuildLegacyScnEvaluator(\"html\")\n\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error %v\\n\", err)\n\t}\n\tout, err := e.Eval(in, \"\")\n\n\tassert.NoError(t, err)\n\n\tif out[\"title\"] != expectedTitle {\n\t\tt.Errorf(\"Unexpected title value got %s, expected %s\\n\", out[\"title\"], expectedTitle)\n\t}\n\tif out[\"description\"] != expectedDescription {\n\t\tt.Errorf(\"Unexpected description value got %s, expected %s\\n\", out[\"description\"], expectedDescription)\n\t}\n}\n\nfunc TestAggregationSupport(t *testing.T) {\n\te := &legacyScnEvaluator{}\n\tif !e.IsAggregationSupported() {\n\t\tt.Errorf(\"Legacy Scan Evaluator should support aggregation by default\\n\")\n\t}\n}\n\nfunc TestBuildAggregatedContent(t *testing.T) {\n\n\texpectedTitle := \"Vulnerability scan report\"\n\texpectedDescription := `<h1>title1</h1>\ndescription1<h1>title2</h1>\ndescription2`\n\n\texpectedUrl := `url1\nurl2`\n\texpectedOwners := []string{\"admin\", \"user\"}\n\n\te, err := BuildLegacyScnEvaluator(\"html\")\n\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error %v\\n\", err)\n\t}\n\tin := []map[string]string{\n\t\t{\n\t\t\t\"title\":       \"title1\",\n\t\t\t\"description\": \"description1\",\n\t\t\t\"url\":         \"url1\",\n\t\t\t\"owners\":      \"admin\",\n\t\t},\n\t\t{\n\t\t\t\"title\":       \"title2\",\n\t\t\t\"description\": \"description2\",\n\t\t\t\"url\":         \"url2\",\n\t\t\t\"owners\":      \"user\",\n\t\t},\n\t}\n\tout, err := e.BuildAggregatedContent(in)\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error %v\\n\", err)\n\t}\n\tif out[\"title\"] != expectedTitle {\n\t\tt.Errorf(\"Unexpected title value got %s, expected %s\\n\", out[\"title\"], expectedTitle)\n\t}\n\tif out[\"description\"] != expectedDescription {\n\t\tt.Errorf(\"Unexpected description value got %s, expected %s\\n\", out[\"description\"], expectedDescription)\n\t}\n\tif out[\"url\"] != expectedUrl {\n\t\tt.Errorf(\"Unexpected description value got %s, expected %s\\n\", out[\"url\"], expectedUrl)\n\t}\n\tactualOwners := strings.Split(out[\"owners\"], \";\")\n\tif len(actualOwners) == len(expectedOwners) {\n\t\tfor _, own := range actualOwners {\n\t\t\tfound := false\n\t\t\tfor _, expOwn := range expectedOwners {\n\t\t\t\tif own == expOwn {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tt.Errorf(\"Unexpected owner: %s\\n\", own)\n\n\t\t\t}\n\t\t}\n\t} else {\n\t\tt.Errorf(\"Unexpected owners value got %s, expected %s\\n\", out[\"owners\"], expectedOwners)\n\t}\n}\n\nfunc TestBuildLegacyScnEvaluator(t *testing.T) {\n\ttests := []struct {\n\t\tlayoutType          string\n\t\texpectedLayoutClass string\n\t\tshouldFail          bool\n\t}{\n\t\t{\"html\", \"*formatting.HtmlProvider\", false},\n\t\t{\"jira\", \"*formatting.JiraLayoutProvider\", false},\n\t\t{\"slack\", \"*formatting.SlackMrkdwnProvider\", false},\n\t\t{\"xml\", \"\", true},\n\t}\n\tfor _, test := range tests {\n\t\te, err := BuildLegacyScnEvaluator(test.layoutType)\n\t\tif err == nil && test.shouldFail {\n\t\t\tt.Fatalf(\"BuildLegacyScnEvaluator should fail for layout type %s but actually didn't return an error\\n\", test.layoutType)\n\t\t} else if err != nil && !test.shouldFail {\n\t\t\tt.Fatalf(\"Unexpected error %v\\n\", err)\n\t\t}\n\t\tif test.shouldFail {\n\t\t\treturn\n\t\t}\n\t\tscnEvaluator, ok := e.(*legacyScnEvaluator)\n\t\tif !ok {\n\t\t\tt.Fatalf(\"Unexpected type of evaluator returned %T\\n\", e)\n\t\t}\n\t\tactualCls := fmt.Sprintf(\"%T\", scnEvaluator.layoutProvider)\n\t\tif actualCls != test.expectedLayoutClass {\n\t\t\tt.Errorf(\"Invalid type of layout provider returned, expected %s, got %s\\n\", test.expectedLayoutClass, actualCls)\n\t\t}\n\t}\n}\nfunc TestToScanImage(t *testing.T) {\n\tvar wrongProp map[bool]string\n\tinp := make(map[string]interface{})\n\n\tinp[\"wrongProp\"] = wrongProp\n\n\t_, err := toScanImage(inp)\n\n\tif err == nil {\n\t\tt.Errorf(\"Error is expected\\n\")\n\t}\n}\n"
  },
  {
    "path": "formatting/htmlprovider.go",
    "content": "package formatting\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype HtmlProvider struct{}\n\nfunc (html *HtmlProvider) P(p string) string {\n\treturn fmt.Sprintf(\"<p>%s</p>\\n\", p)\n}\n\nfunc (html *HtmlProvider) TitleH1(title string) string {\n\treturn fmt.Sprintf(\"<h1>%s</h1>\\n\", title)\n}\n\nfunc (html *HtmlProvider) TitleH2(title string) string {\n\treturn fmt.Sprintf(\"<h2>%s</h2>\\n\", title)\n}\n\nfunc (html *HtmlProvider) TitleH3(title string) string {\n\treturn fmt.Sprintf(\"<h3>%s</h3>\\n\", title)\n}\n\nfunc (html *HtmlProvider) ColourText(text, color string) string {\n\treturn fmt.Sprintf(\"<span style='color:%s'>%s</span>\", color, text)\n}\n\nfunc (html *HtmlProvider) Table(rows [][]string) string {\n\ttable := make([]string, 0)\n\ttable = append(table, \"<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\")\n\tfor i, r := range rows {\n\t\tvar tag string\n\t\tif i == 0 {\n\t\t\ttag = \"TH\"\n\t\t} else {\n\t\t\ttag = \"TD\"\n\t\t}\n\t\ttable = append(table, \"<TR>\")\n\t\tvar rowBuilder bytes.Buffer\n\t\tfor _, field := range r {\n\t\t\trowBuilder.WriteString(fmt.Sprintf(\"<%s style='padding: 5px;'>%s</%s>\", tag, field, tag))\n\t\t}\n\t\ttable = append(table, rowBuilder.String())\n\t\ttable = append(table, \"</TR>\")\n\t}\n\n\ttable = append(table, \"</TABLE>\\n\")\n\treturn strings.Join(table, \"\\n\")\n}\n\nfunc (html *HtmlProvider) A(url, title string) string {\n\treturn fmt.Sprintf(\"<a href='%s'>%s</a>\", url, title)\n}\n"
  },
  {
    "path": "formatting/htmlprovider_test.go",
    "content": "package formatting\n\nimport (\n\t\"testing\"\n)\n\nfunc TestHtmlProvider_Table(t *testing.T) {\n\tvar tests = []tableTest{\n\t\t{\n\t\t\tsource: [][]string{\n\t\t\t\t{\"Header1\", \"Header2\"},\n\t\t\t\t{\"Field1\", \"Field2\"},\n\t\t\t},\n\t\t\tresult: `<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n<TR>\n<TH style='padding: 5px;'>Header1</TH><TH style='padding: 5px;'>Header2</TH>\n</TR>\n<TR>\n<TD style='padding: 5px;'>Field1</TD><TD style='padding: 5px;'>Field2</TD>\n</TR>\n</TABLE>\n`,\n\t\t},\n\t}\n\ttableTesting(tests, t, new(HtmlProvider))\n}\n\nfunc TestHtmlProviderTags(t *testing.T) {\n\ttests := []tagsTest{\n\t\t{\n\t\t\t\"Lorem Ipsum\",\n\t\t\t\"red\",\n\t\t\t\"url\",\n\t\t\t\"<span style='color:red'>Lorem Ipsum</span>\",\n\t\t\t\"<h1>Lorem Ipsum</h1>\\n\",\n\t\t\t\"<h2>Lorem Ipsum</h2>\\n\",\n\t\t\t\"<h3>Lorem Ipsum</h3>\\n\",\n\t\t\t\"<p>Lorem Ipsum</p>\\n\",\n\t\t\t\"<a href='url'>Lorem Ipsum</a>\",\n\t\t},\n\t}\n\ttagsTesting(tests, t, new(HtmlProvider))\n}\n"
  },
  {
    "path": "formatting/jiraprovider.go",
    "content": "package formatting\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype JiraLayoutProvider struct{}\n\nfunc (jira *JiraLayoutProvider) P(p string) string {\n\treturn fmt.Sprintf(\"%s\\n\", p)\n}\n\nfunc (jira *JiraLayoutProvider) TitleH1(title string) string {\n\treturn fmt.Sprintf(\"h1. %s\\n\", title)\n}\n\nfunc (jira *JiraLayoutProvider) TitleH2(title string) string {\n\treturn fmt.Sprintf(\"h2. %s\\n\", title)\n}\n\nfunc (jira *JiraLayoutProvider) TitleH3(title string) string {\n\treturn fmt.Sprintf(\"h3. %s\\n\", title)\n}\n\nfunc (jira *JiraLayoutProvider) ColourText(text, color string) string {\n\treturn fmt.Sprintf(\"{color:%s}%s{color}\", color, text)\n}\n\nfunc (jira *JiraLayoutProvider) Table(rows [][]string) string {\n\tif len(rows) == 0 {\n\t\treturn \"\"\n\t}\n\tvar builder bytes.Buffer\n\tfor i, row := range rows {\n\t\tif i == 0 {\n\t\t\tfmt.Fprintf(&builder, \"||%s||\\n\", strings.Join(row, \"||\"))\n\t\t} else {\n\t\t\tfmt.Fprintf(&builder, \"|%s|\\n\", strings.Join(row, \"|\"))\n\t\t}\n\t}\n\tbuilder.WriteString(\"\\n\")\n\treturn builder.String()\n}\n\nfunc (jira *JiraLayoutProvider) A(url, title string) string {\n\treturn fmt.Sprintf(\"[%s|%s]\", title, url)\n}\n"
  },
  {
    "path": "formatting/jiraprovider_test.go",
    "content": "package formatting\n\nimport \"testing\"\n\nfunc TestJiraLayoutProvider_Tags(t *testing.T) {\n\ttests := []tagsTest{\n\t\t{\n\t\t\t\"Lorem Ipsum\",\n\t\t\t\"red\",\n\t\t\t\"url\",\n\t\t\t\"{color:red}Lorem Ipsum{color}\",\n\t\t\t\"h1. Lorem Ipsum\\n\",\n\t\t\t\"h2. Lorem Ipsum\\n\",\n\t\t\t\"h3. Lorem Ipsum\\n\",\n\t\t\t\"Lorem Ipsum\\n\",\n\t\t\t\"[Lorem Ipsum|url]\",\n\t\t},\n\t}\n\ttagsTesting(tests, t, new(JiraLayoutProvider))\n}\n\nfunc TestJiraLayoutProvider_Table(t *testing.T) {\n\tvar tests = []tableTest{\n\t\t{\n\t\t\tsource: [][]string{\n\t\t\t\t{\"Header1\", \"Header2\"},\n\t\t\t\t{\"Field1\", \"Field2\"},\n\t\t\t},\n\t\t\tresult: `||Header1||Header2||\n|Field1|Field2|\n\n`,\n\t\t},\n\t\t{\n\t\t\tsource: nil,\n\t\t\tresult: \"\",\n\t\t},\n\t}\n\ttableTesting(tests, t, new(JiraLayoutProvider))\n}\n"
  },
  {
    "path": "formatting/markup_test.go",
    "content": "package formatting\n\nimport (\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\ntype tagsTest struct {\n\tsource                       string\n\tcolor                        string\n\tlink                         string\n\tcolourText, h1, h2, h3, p, a string\n}\n\ntype tableTest struct {\n\tsource [][]string\n\tresult string\n}\n\nfunc tagsTesting(tests []tagsTest, t *testing.T, provider layout.LayoutProvider) {\n\tfor _, test := range tests {\n\t\tif c := provider.ColourText(test.source, test.color); c != test.colourText {\n\t\t\tt.Errorf(\"Wrong colorur text\\nWaited: %q\\n Result: %q\", test.colourText, c)\n\t\t}\n\t\tif h1 := provider.TitleH1(test.source); h1 != test.h1 {\n\t\t\tt.Errorf(\"Wrong H1 formatting for %q\\nWaited: %q\\n Result: %q\", test.source, test.h1, h1)\n\t\t}\n\t\tif h2 := provider.TitleH2(test.source); h2 != test.h2 {\n\t\t\tt.Errorf(\"Wrong H2 formatting for %q\\nWaited: %q\\n Result: %q\", test.source, test.h2, h2)\n\t\t}\n\t\tif h3 := provider.TitleH3(test.source); h3 != test.h3 {\n\t\t\tt.Errorf(\"Wrong H3 formatting for %q\\nWaited: %q\\n Result: %q\", test.source, test.h3, h3)\n\t\t}\n\t\tif p := provider.P(test.source); p != test.p {\n\t\t\tt.Errorf(\"Wrong P formatting for %q\\nWaited: %q\\n Result: %q\", test.source, test.p, p)\n\t\t}\n\t\tif a := provider.A(test.link, test.source); a != test.a {\n\t\t\tt.Errorf(\"Wrong P formatting for link %q (%q)\\nWaited: %q\\n Result: %q\",\n\t\t\t\ttest.link, test.source, test.a, a)\n\t\t}\n\t}\n}\n\nfunc tableTesting(tests []tableTest, t *testing.T, provider layout.LayoutProvider) {\n\tfor _, test := range tests {\n\t\tif got := provider.Table(test.source); got != test.result {\n\t\t\tt.Errorf(\"Error: html.Table(test.Source)\\nResult: %s\\nWaited: %s\\n\", got, test.result)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "formatting/slackmrkdwnprovider.go",
    "content": "package formatting\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc getMrkdwnText(text string) string {\n\tblock := &data.SlackBlock{\n\t\tTypeField: \"section\",\n\t\tTextField: &data.SlackTextBlock{\n\t\t\tTypeField: \"mrkdwn\",\n\t\t\tTextField: text,\n\t\t},\n\t}\n\tresult, err := json.Marshal(block)\n\tif err != nil {\n\t\tlog.Printf(\"SlackMrkdwnProvider Error: %v\", err)\n\t\treturn \"\"\n\t}\n\tresult = append(result, ',')\n\treturn string(result)\n}\n\ntype SlackMrkdwnProvider struct{}\n\nfunc (mrkdwn *SlackMrkdwnProvider) TitleH1(title string) string {\n\treturn getMrkdwnText(fmt.Sprintf(\"*%s*\", title))\n}\n\nfunc (mrkdwn *SlackMrkdwnProvider) TitleH2(title string) string {\n\treturn getMrkdwnText(fmt.Sprintf(\"*%s*\", title))\n}\n\nfunc (mrkdwn *SlackMrkdwnProvider) TitleH3(title string) string {\n\treturn mrkdwn.TitleH2(title)\n}\n\nfunc (mrkdwn *SlackMrkdwnProvider) ColourText(text, color string) string {\n\treturn fmt.Sprintf(\"*%s*\", text)\n}\n\nfunc (mrkdwn *SlackMrkdwnProvider) Table(rows [][]string) string {\n\tif len(rows) == 0 {\n\t\treturn \"\"\n\t}\n\tvar builder bytes.Buffer\n\n\tfields := &data.SlackBlock{\n\t\tTypeField: \"section\",\n\t}\n\tif len(rows) == 2 && len(rows[0]) == 5 {\n\t\tfields.Fields = make([]data.SlackTextBlock, 2*len(rows[0]))\n\t\tfor i, r := range rows {\n\t\t\tfor j, f := range r {\n\t\t\t\tif i == 0 {\n\t\t\t\t\tfields.Fields[j*2] = data.SlackTextBlock{\n\t\t\t\t\t\tTypeField: \"mrkdwn\",\n\t\t\t\t\t\tTextField: fmt.Sprintf(\"*%s*\", f),\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tfields.Fields[j*2+1] = data.SlackTextBlock{\n\t\t\t\t\t\tTypeField: \"mrkdwn\",\n\t\t\t\t\t\tTextField: f,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\ttotalRows := len(rows)\n\t\tfor line, r := range rows {\n\t\t\tif line%5 == 0 {\n\t\t\t\tif fields.Fields != nil {\n\t\t\t\t\tblock, err := json.Marshal(fields)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"SlackMrkdwnProvider Error: %v\", err)\n\t\t\t\t\t\treturn \"\"\n\t\t\t\t\t}\n\t\t\t\t\tbuilder.Write(block)\n\t\t\t\t\tbuilder.WriteByte(',')\n\t\t\t\t}\n\t\t\t\tfields = new(data.SlackBlock)\n\t\t\t\tfields.TypeField = \"section\"\n\t\t\t\tcurrent := 5\n\t\t\t\tif (totalRows - line) < 5 {\n\t\t\t\t\tcurrent = totalRows - line\n\t\t\t\t}\n\t\t\t\tfields.Fields = make([]data.SlackTextBlock, current*2)\n\t\t\t}\n\t\t\tvar cell1, cell2 bytes.Buffer\n\t\t\tfor j, f := range r {\n\t\t\t\tbold := \"\"\n\t\t\t\tif line == 0 {\n\t\t\t\t\tbold = \"*\"\n\t\t\t\t}\n\t\t\t\tswitch j {\n\t\t\t\tcase 0:\n\t\t\t\t\tfmt.Fprintf(&cell1, \"%s%s%s\", bold, f, bold)\n\t\t\t\tcase 1:\n\t\t\t\t\tif rows[0][0] == \"#\" {\n\t\t\t\t\t\tfmt.Fprintf(&cell1, \" %s%s%s\", bold, f, bold)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfmt.Fprintf(&cell2, \"%s%s%s\", bold, f, bold)\n\t\t\t\t\t\tif len(r) > 2 {\n\t\t\t\t\t\t\tfmt.Fprint(&cell2, \" / \")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tdefault:\n\t\t\t\t\tif j > 2 {\n\t\t\t\t\t\tcell2.WriteString(\" / \")\n\t\t\t\t\t}\n\t\t\t\t\tfmt.Fprintf(&cell2, \"%s%s%s\", bold, f, bold)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfields.Fields[(line%5)*2] = data.SlackTextBlock{\n\t\t\t\tTypeField: \"mrkdwn\",\n\t\t\t\tTextField: cell1.String(),\n\t\t\t}\n\t\t\tfields.Fields[(line%5)*2+1] = data.SlackTextBlock{\n\t\t\t\tTypeField: \"mrkdwn\",\n\t\t\t\tTextField: cell2.String(),\n\t\t\t}\n\t\t}\n\t}\n\tresult, err := json.Marshal(fields)\n\tif err != nil {\n\t\tlog.Printf(\"SlackMrkdwnProvider Error: %v\", err)\n\t\treturn \"\"\n\t}\n\tbuilder.Write(result)\n\tbuilder.WriteByte(',')\n\treturn builder.String()\n}\n\nfunc (mrkdwn *SlackMrkdwnProvider) P(p string) string {\n\treturn getMrkdwnText(p)\n}\n\nfunc (mrkdwn *SlackMrkdwnProvider) A(url, title string) string {\n\treturn fmt.Sprintf(\"<%s|%s>\", url, title)\n}\n"
  },
  {
    "path": "formatting/slackmrkdwnprovider_test.go",
    "content": "package formatting\n\nimport \"testing\"\n\nfunc TestSlackMrkdwn(t *testing.T) {\n\ttests := []tagsTest{\n\t\t{\n\t\t\t\"Lorem Ipsum\",\n\t\t\t\"red\",\n\t\t\t\"url\",\n\t\t\t\"*Lorem Ipsum*\",\n\t\t\t\"{\\\"type\\\":\\\"section\\\",\\\"text\\\":{\\\"type\\\":\\\"mrkdwn\\\",\\\"text\\\":\\\"*Lorem Ipsum*\\\"}},\",\n\t\t\t\"{\\\"type\\\":\\\"section\\\",\\\"text\\\":{\\\"type\\\":\\\"mrkdwn\\\",\\\"text\\\":\\\"*Lorem Ipsum*\\\"}},\",\n\t\t\t\"{\\\"type\\\":\\\"section\\\",\\\"text\\\":{\\\"type\\\":\\\"mrkdwn\\\",\\\"text\\\":\\\"*Lorem Ipsum*\\\"}},\",\n\t\t\t\"{\\\"type\\\":\\\"section\\\",\\\"text\\\":{\\\"type\\\":\\\"mrkdwn\\\",\\\"text\\\":\\\"Lorem Ipsum\\\"}},\",\n\t\t\t\"<url|Lorem Ipsum>\",\n\t\t},\n\t}\n\ttagsTesting(tests, t, new(SlackMrkdwnProvider))\n}\n\nfunc TestSlackMrkdwnProvider_Table(t *testing.T) {\n\tvar tests = []tableTest{\n\t\t{\n\t\t\tsource: [][]string{\n\t\t\t\t{\"Header1\", \"Header2\"},\n\t\t\t\t{\"Field1\", \"Field2\"},\n\t\t\t},\n\t\t\tresult: `{\"type\":\"section\",\"fields\":[{\"type\":\"mrkdwn\",\"text\":\"*Header1*\"},{\"type\":\"mrkdwn\",\"text\":\"*Header2*\"},{\"type\":\"mrkdwn\",\"text\":\"Field1\"},{\"type\":\"mrkdwn\",\"text\":\"Field2\"}]},`,\n\t\t},\n\t\t{\n\t\t\tsource: [][]string{\n\t\t\t\t{\"Header1\", \"Header2\", \"Header3\"},\n\t\t\t\t{\"Field1\", \"Field2\", \"Field3\"},\n\t\t\t},\n\t\t\tresult: `{\"type\":\"section\",\"fields\":[{\"type\":\"mrkdwn\",\"text\":\"*Header1*\"},{\"type\":\"mrkdwn\",\"text\":\"*Header2* / *Header3*\"},{\"type\":\"mrkdwn\",\"text\":\"Field1\"},{\"type\":\"mrkdwn\",\"text\":\"Field2 / Field3\"}]},`,\n\t\t},\n\t\t{\n\t\t\tsource: [][]string{\n\t\t\t\t{\"Critical\", \"High\", \"Medium\", \"Low\", \"Negligible\"},\n\t\t\t\t{\"Field10\", \"Field5\", \"Field3\", \"F27\", \"F232\"},\n\t\t\t},\n\t\t\tresult: `{\"type\":\"section\",\"fields\":[{\"type\":\"mrkdwn\",\"text\":\"*Critical*\"},{\"type\":\"mrkdwn\",\"text\":\"Field10\"},{\"type\":\"mrkdwn\",\"text\":\"*High*\"},{\"type\":\"mrkdwn\",\"text\":\"Field5\"},{\"type\":\"mrkdwn\",\"text\":\"*Medium*\"},{\"type\":\"mrkdwn\",\"text\":\"Field3\"},{\"type\":\"mrkdwn\",\"text\":\"*Low*\"},{\"type\":\"mrkdwn\",\"text\":\"F27\"},{\"type\":\"mrkdwn\",\"text\":\"*Negligible*\"},{\"type\":\"mrkdwn\",\"text\":\"F232\"}]},`,\n\t\t},\n\t}\n\ttableTesting(tests, t, new(SlackMrkdwnProvider))\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/aquasecurity/postee/v2\n\ngo 1.18\n\nrequire (\n\tgithub.com/DependencyTrack/client-go v0.11.0\n\tgithub.com/PagerDuty/go-pagerduty v1.5.1\n\tgithub.com/aquasecurity/go-jira v0.0.0-20230705211506-0cd878ce5449\n\tgithub.com/aws/aws-sdk-go-v2 v1.16.11\n\tgithub.com/aws/aws-sdk-go-v2/config v1.17.1\n\tgithub.com/aws/aws-sdk-go-v2/service/securityhub v1.22.7\n\tgithub.com/aws/smithy-go v1.12.1\n\tgithub.com/docker/docker v20.10.24+incompatible\n\tgithub.com/ghodss/yaml v1.0.0\n\tgithub.com/google/uuid v1.3.0\n\tgithub.com/gorilla/mux v1.8.0\n\tgithub.com/nats-io/nats-server/v2 v2.7.4\n\tgithub.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d\n\tgithub.com/nats-io/nkeys v0.3.0\n\tgithub.com/open-policy-agent/opa v0.45.0\n\tgithub.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799\n\tgithub.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10\n\tgithub.com/spf13/cobra v1.5.0\n\tgithub.com/stretchr/testify v1.8.2\n\tgithub.com/tidwall/gjson v1.14.0\n\tgo.etcd.io/bbolt v1.3.6\n\tk8s.io/api v0.23.3\n\tk8s.io/apimachinery v0.23.3\n\tk8s.io/client-go v0.23.3\n)\n\nrequire (\n\tgithub.com/Microsoft/go-winio v0.5.1 // indirect\n\tgithub.com/OneOfOne/xxhash v1.2.8 // indirect\n\tgithub.com/agnivade/levenshtein v1.1.1 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.12.14 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.11.17 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.16.13 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/docker/distribution v2.8.2+incompatible // indirect\n\tgithub.com/docker/go-connections v0.4.0 // indirect\n\tgithub.com/docker/go-units v0.4.0 // indirect\n\tgithub.com/evanphx/json-patch v4.12.0+incompatible // indirect\n\tgithub.com/fatih/structs v1.1.0 // indirect\n\tgithub.com/go-logr/logr v1.2.3 // indirect\n\tgithub.com/gobwas/glob v0.2.3 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.2.0 // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/google/go-cmp v0.5.9 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/gofuzz v1.2.0 // indirect\n\tgithub.com/googleapis/gnostic v0.5.5 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.5.3 // indirect\n\tgithub.com/imdario/mergo v0.3.12 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.14.4 // indirect\n\tgithub.com/minio/highwayhash v1.0.2 // indirect\n\tgithub.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 // indirect\n\tgithub.com/nats-io/nuid v1.0.1 // indirect\n\tgithub.com/onsi/ginkgo v1.16.5 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/tchap/go-patricia/v2 v2.3.1 // indirect\n\tgithub.com/tidwall/match v1.1.1 // indirect\n\tgithub.com/tidwall/pretty v1.2.0 // indirect\n\tgithub.com/trivago/tgo v1.0.7 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/yashtewari/glob-intersection v0.1.0 // indirect\n\tgolang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect\n\tgolang.org/x/net v0.7.0 // indirect\n\tgolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect\n\tgolang.org/x/sys v0.5.0 // indirect\n\tgolang.org/x/term v0.5.0 // indirect\n\tgolang.org/x/text v0.7.0 // indirect\n\tgolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/protobuf v1.28.1 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tgotest.tools/v3 v3.1.0 // indirect\n\tk8s.io/klog/v2 v2.30.0 // indirect\n\tk8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect\n\tk8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect\n\tsigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect\n\tsigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect\n\tsigs.k8s.io/yaml v1.3.0 // indirect\n)\n\nreplace (\n\tgithub.com/docker/cli v0.0.0-20191017083524-a8ff7f821017 => github.com/docker/cli v20.10.9+incompatible\n\tgithub.com/satori/go.uuid v1.2.0 => github.com/satori/go.uuid v1.2.1-0.20181016170032-d91630c85102\n\tgolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 => golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/DependencyTrack/client-go v0.11.0 h1:1g+eHC8nJyIzi68zcs+dr3OHRvS1aC+4Uy3YKA0JJhc=\ngithub.com/DependencyTrack/client-go v0.11.0/go.mod h1:XLZnOksOs56Svq+K4xmBkN8U97gpP7r1BkhCc/xA8Iw=\ngithub.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=\ngithub.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=\ngithub.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=\ngithub.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=\ngithub.com/PagerDuty/go-pagerduty v1.5.1 h1:zpMQ8WwWlUahipB2q+ERVIA9D0/ti8kvsQUSagCK86g=\ngithub.com/PagerDuty/go-pagerduty v1.5.1/go.mod h1:txr8VbObXdk2RkqF+C2an4qWssdGY99fK26XYUDjh+4=\ngithub.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=\ngithub.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=\ngithub.com/aquasecurity/go-jira v0.0.0-20230705211506-0cd878ce5449 h1:iiLF0O6h/Y5bdWSmxlb2EhdozYa5HTn+asKHSqr0R0M=\ngithub.com/aquasecurity/go-jira v0.0.0-20230705211506-0cd878ce5449/go.mod h1:IHtKzIAdk0t3Xse7rJSY7pJlA8gB7lqY2b4l5WYZYsk=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=\ngithub.com/aws/aws-sdk-go-v2 v1.16.11 h1:xM1ZPSvty3xVmdxiGr7ay/wlqv+MWhH0rMlyLdbC0YQ=\ngithub.com/aws/aws-sdk-go-v2 v1.16.11/go.mod h1:WTACcleLz6VZTp7fak4EO5b9Q4foxbn+8PIz3PmyKlo=\ngithub.com/aws/aws-sdk-go-v2/config v1.17.1 h1:BWxTjokU/69BZ4DnLrZco6OvBDii6ToEdfBL/y5I1nA=\ngithub.com/aws/aws-sdk-go-v2/config v1.17.1/go.mod h1:uOxDHjBemNTF2Zos+fgG0NNfE86wn1OAHDTGxjMEYi0=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.12.14 h1:AtVG/amkjbDBfnPr/tuW2IG18HGNznP6L12Dx0rLz+Q=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.12.14/go.mod h1:opAndTyq+YN7IpVG57z2CeNuXSQMqTYxGGlYH0m0RMY=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 h1:wgJBHO58Pc1V1QAnzdVM3JK3WbE/6eUF0JxCZ+/izz0=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12/go.mod h1:aZ4vZnyUuxedC7eD4JyEHpGnCz+O2sHQEx3VvAwklSE=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 h1:OmiwoVyLKEqqD5GvB683dbSqxiOfvx4U2lDZhG2Esc4=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18/go.mod h1:348MLhzV1GSlZSMusdwQpXKbhD7X2gbI/TxwAPKkYZQ=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 h1:5mvQDtNWtI6H56+E4LUnLWEmATMB7oEh+Z9RurtIuC0=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12/go.mod h1:ckaCVTEdGAxO6KwTGzgskxR1xM+iJW4lxMyDFVda2Fc=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 h1:g5qq9sgtEzt2szMaDqQO6fqKe026T6dHTFJp5NsPzkQ=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.3.19/go.mod h1:cVHo8KTuHjShb9V8/VjH3S/8+xPu16qx8fdGwmotJhE=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 h1:7iPTTX4SAI2U2VOogD7/gmHlsgnYSgoNHt7MSQXtG2M=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12/go.mod h1:1TODGhheLWjpQWSuhYuAUWYTCKwEjx2iblIFKDHjeTc=\ngithub.com/aws/aws-sdk-go-v2/service/securityhub v1.22.7 h1:NXJ6NdzJvXkUSwmwbSRtWPvTfIg5BueQ2Z1vid8o9CQ=\ngithub.com/aws/aws-sdk-go-v2/service/securityhub v1.22.7/go.mod h1:byhebHID81uPiHS2NQcZrKxOiB2roj3OOcWMvdxxjmk=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.11.17 h1:pXxu9u2z1UqSbjO9YA8kmFJBhFc1EVTDaf7A+S+Ivq8=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.11.17/go.mod h1:mS5xqLZc/6kc06IpXn5vRxdLaED+jEuaSRv5BxtnsiY=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.16.13 h1:dl8T0PJlN92rvEGOEUiD0+YPYdPEaCZK0TqHukvSfII=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.16.13/go.mod h1:Ru3QVMLygVs/07UQ3YDur1AQZZp2tUNje8wfloFttC0=\ngithub.com/aws/smithy-go v1.12.1 h1:yQRC55aXN/y1W10HgwHle01DRuV9Dpf31iGkotjt3Ag=\ngithub.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bytecodealliance/wasmtime-go v1.0.0 h1:9u9gqaUiaJeN5IoD1L7egD8atOnTGyJcNp8BhkL9cUU=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=\ngithub.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=\ngithub.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=\ngithub.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=\ngithub.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=\ngithub.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=\ngithub.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE=\ngithub.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=\ngithub.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=\ngithub.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=\ngithub.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=\ngithub.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=\ngithub.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=\ngithub.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=\ngithub.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=\ngithub.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=\ngithub.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=\ngithub.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=\ngithub.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=\ngithub.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=\ngithub.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=\ngithub.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=\ngithub.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=\ngithub.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=\ngithub.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-retryablehttp v0.5.1/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=\ngithub.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4=\ngithub.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=\ngithub.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=\ngithub.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=\ngithub.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=\ngithub.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=\ngithub.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY=\ngithub.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=\ngithub.com/nats-io/nats-server/v2 v2.7.4 h1:c+BZJ3rGzUKCBIM4IXO8uNT2u1vajGbD1kPA6wqCEaM=\ngithub.com/nats-io/nats-server/v2 v2.7.4/go.mod h1:1vZ2Nijh8tcyNe8BDVyTviCd9NYzRbubQYiEHsvOQWc=\ngithub.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d h1:zJf4l8Kp67RIZhoVeniSLZs69SHNgjLHz0aNsqPPlx8=\ngithub.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=\ngithub.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=\ngithub.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=\ngithub.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/open-policy-agent/opa v0.45.0 h1:P5nuhVRtR+e58fk3CMMbiqr6ZFyWQPNOC3otsorGsFs=\ngithub.com/open-policy-agent/opa v0.45.0/go.mod h1:/OnsYljNEWJ6DXeFOOnoGn8CvwZGMUS4iRqzYdJvmBI=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec=\ngithub.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10 h1:qHnitdkr8TN/irubnQM8ml/udTyAxo6j5v61H7+TV3k=\ngithub.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10/go.mod h1:4OjcxgwdXzezqytxN534MooNmrxRD50geWZxTD7845s=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=\ngithub.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=\ngithub.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=\ngithub.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=\ngithub.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=\ngithub.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=\ngithub.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=\ngithub.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=\ngithub.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=\ngithub.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=\ngithub.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=\ngithub.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg=\ngithub.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngo.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=\ngo.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=\ngolang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=\ngolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=\ngotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk=\ngotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.23.3 h1:KNrME8KHGr12Ozjf8ytOewKzZh6hl/hHUZeHddT3a38=\nk8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ=\nk8s.io/apimachinery v0.23.3 h1:7IW6jxNzrXTsP0c8yXz2E5Yx/WTzVPTsHIx/2Vm0cIk=\nk8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM=\nk8s.io/client-go v0.23.3 h1:23QYUmCQ/W6hW78xIwm3XqZrrKZM+LWDqW2zfo+szJs=\nk8s.io/client-go v0.23.3/go.mod h1:47oMd+YvAOqZM7pcQ6neJtBiFH7alOyfunYN48VsmwE=\nk8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=\nk8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=\nk8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=\nk8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw=\nk8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=\nk8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4=\nk8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk=\nk8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nk8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE=\nk8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s=\nsigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y=\nsigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\nsigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=\nsigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=\n"
  },
  {
    "path": "integration/controller_runner_test.go",
    "content": "//go:build integration\n\npackage integration\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/controller\"\n\t\"github.com/aquasecurity/postee/v2/router\"\n\t\"github.com/aquasecurity/postee/v2/runner\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nconst (\n\tRunnerConfig = `\nroutes:\n- name: terminate-and-notify\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  actions: [terminate-pod]\n  plugins: {}\n\nactions:\n- name: terminate-pod\n  runs-on: \"test-runner-1\"\n  type: exec\n  enable: true\n  exec-script: |\n    #!/bin/sh\n    PID=$(echo $POSTEE_EVENT | jq -r .Context.hostName)\n    kubectl delete pod $PID     # If terminating a K8s pod\n    # pkill -SIGTERM $PID       # If terminating a UNIX process\n`\n)\n\nfunc TestControllerRunner_Happy(t *testing.T) {\n\ttestCases := []struct {\n\t\tname           string\n\t\tcCfg           controller.Controller\n\t\trCfg           runner.Runner\n\t\texpectedConfig string\n\t}{\n\t\t{\n\t\t\tname: \"no tls, no auth\",\n\t\t\tcCfg: controller.Controller{\n\t\t\t\tControllerURL: \"nats://0.0.0.0:17777\",\n\t\t\t\tRunnerName:    \"test-runner-1\",\n\t\t\t},\n\t\t\trCfg: runner.Runner{\n\t\t\t\tControllerURL: \"nats://0.0.0.0:17777\",\n\t\t\t\tRunnerName:    \"test-runner-1\",\n\t\t\t},\n\t\t\texpectedConfig: RunnerConfig,\n\t\t},\n\t\t{\n\t\t\tname: \"with tls, no auth\",\n\t\t\tcCfg: controller.Controller{\n\t\t\t\tControllerURL:         \"tls://0.0.0.0:18888\",\n\t\t\t\tRunnerName:            \"test-runner-1\",\n\t\t\t\tControllerTLSKeyPath:  \"goldens/server-key.pem\",\n\t\t\t\tControllerTLSCertPath: \"goldens/server-cert.pem\",\n\t\t\t\tControllerCAFile:      \"goldens/rootCA.pem\",\n\t\t\t},\n\t\t\trCfg: runner.Runner{\n\t\t\t\tControllerURL:     \"tls://0.0.0.0:18888\",\n\t\t\t\tRunnerName:        \"test-runner-1\",\n\t\t\t\tRunnerCARootPath:  \"goldens/rootCA.pem\",\n\t\t\t\tRunnerTLSCertPath: \"goldens/client-cert.pem\",\n\t\t\t\tRunnerTLSKeyPath:  \"goldens/client-key.pem\",\n\t\t\t},\n\t\t\texpectedConfig: RunnerConfig,\n\t\t},\n\t\t{\n\t\t\tname: \"with tls, with auth\",\n\t\t\tcCfg: controller.Controller{\n\t\t\t\tControllerURL:          \"tls://0.0.0.0:19999\",\n\t\t\t\tRunnerName:             \"test-runner-1\",\n\t\t\t\tControllerTLSKeyPath:   \"goldens/server-key.pem\",\n\t\t\t\tControllerTLSCertPath:  \"goldens/server-cert.pem\",\n\t\t\t\tControllerCAFile:       \"goldens/rootCA.pem\",\n\t\t\t\tControllerSeedFilePath: \"goldens/test-seed.txt\",\n\t\t\t},\n\t\t\trCfg: runner.Runner{\n\t\t\t\tControllerURL:      \"tls://0.0.0.0:19999\",\n\t\t\t\tRunnerName:         \"test-runner-1\",\n\t\t\t\tRunnerCARootPath:   \"goldens/rootCA.pem\",\n\t\t\t\tRunnerTLSCertPath:  \"goldens/client-cert.pem\",\n\t\t\t\tRunnerTLSKeyPath:   \"goldens/client-key.pem\",\n\t\t\t\tRunnerSeedFilePath: \"goldens/test-seed.txt\",\n\t\t\t},\n\t\t\texpectedConfig: RunnerConfig,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trtr := router.Instance()\n\n\t\t\trequire.NoError(t, tc.cCfg.Setup(rtr))\n\t\t\trequire.NoError(t, rtr.Start(\"goldens/simple.yaml\"))\n\n\t\t\tf, err := ioutil.TempFile(\"\", \"TestRunner_Setup-*\")\n\t\t\tdefer func() { os.Remove(f.Name()) }()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.NoError(t, tc.rCfg.Setup(rtr, f))\n\n\t\t\tgot, err := ioutil.ReadFile(f.Name())\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.YAMLEq(t, tc.expectedConfig, string(got))\n\n\t\t\trtr.Terminate()\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "integration/goldens/client-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEUjCCArqgAwIBAgIRAPLnJ75aAxz0TfngEX3vEikwDQYJKoZIhvcNAQELBQAw\nZzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMR4wHAYDVQQLDBV2YWdy\nYW50QHVidW50dS1pbXBpc2gxJTAjBgNVBAMMHG1rY2VydCB2YWdyYW50QHVidW50\ndS1pbXBpc2gwHhcNMjIwNTE3MjAxNjU1WhcNMjQwODE3MjAxNjU1WjBJMScwJQYD\nVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxHjAcBgNVBAsMFXZh\nZ3JhbnRAdWJ1bnR1LWltcGlzaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC\nggEBALlS5IVD77EWWok1OBzw/k7LNvkEsykSVfU9Y0KvbpR8nOqt877j/PHohFIr\n5LdMFjeNWqgwAXhpPcjUKXd7irYZ4yEJJRyGYD4VuByCVq9pPd72nbf3ViK6V890\n0+JmrNXyFVLYgYuOXuzYgGa3hyx6qRvcTTacHKfwbGCn05ZnFIX58+Pe+cRoXLAc\n10iytPPxtlu6r7bV0OYd3B3RcH2lW/eufNijYGlpGwtAatsN7RkUpW/PUOqtDVFt\nOhmxMeXGJkPZpe8rgXPhzSVsCv8YvPISpj16tGCIBADDInyVneMAS6mfMzuOXn7L\nhjSPFd87KS1PvhWGJlKqVqszd4cCAwEAAaOBljCBkzAOBgNVHQ8BAf8EBAMCBaAw\nJwYDVR0lBCAwHgYIKwYBBQUHAwIGCCsGAQUFBwMBBggrBgEFBQcDBDAfBgNVHSME\nGDAWgBQbhjMiZMT6ZfLuxSV+BFqynUBfTDA3BgNVHREEMDAugglsb2NhbGhvc3SB\nD2VtYWlsQGxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsF\nAAOCAYEARan69HAf3nbwF5gqn11F3J/Li2rW3KHcDKy8fVzCvWajoXfAW3VMfqIO\no5FZ3xYHXENvpY3AclxZgNZrN7B/uOac4iiI0c+NcyzaztOtgD72VDalHl9Cp6N9\nxp/bF48OPDY9f33+fRieXRt8pGKzRlb9xsuvKJyCQvXrEw9S21eg7LrNk4F9pCQa\ne9PL1HOygvpltH/9h/jqL4lnWp/8GBHMbEAllk8gLCHPMFl5LSGrOoqfJDOxPphZ\nkuduHby0KhlEUejiV0mG0MIof4uagZTm7mQ7nguvxNu6wW6gU8Pwp6Lw//7bwkWc\nL4NtlHk4NhS9yB7uL/STVC2ZMH2Nfy2z9j0jpIjZ4kyvV6Zd4j9dVbvNzD/op6Y9\nKNGq/g3aYqPHrn4MZ/1FiQBY2YtEHSSBbc2YJxBjL1fZJ0gGYVYB5o0qEepq41w4\nQYprUb+TrCQ0NX8i9iJla2s2CXwLUdRFObvlp9Q5SswMuWjYXNNNrk6UryKRnc7d\npE9OyCLq\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "integration/goldens/client-key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5UuSFQ++xFlqJ\nNTgc8P5Oyzb5BLMpElX1PWNCr26UfJzqrfO+4/zx6IRSK+S3TBY3jVqoMAF4aT3I\n1Cl3e4q2GeMhCSUchmA+FbgcglavaT3e9p2391YiulfPdNPiZqzV8hVS2IGLjl7s\n2IBmt4cseqkb3E02nByn8Gxgp9OWZxSF+fPj3vnEaFywHNdIsrTz8bZbuq+21dDm\nHdwd0XB9pVv3rnzYo2BpaRsLQGrbDe0ZFKVvz1DqrQ1RbToZsTHlxiZD2aXvK4Fz\n4c0lbAr/GLzyEqY9erRgiAQAwyJ8lZ3jAEupnzM7jl5+y4Y0jxXfOyktT74VhiZS\nqlarM3eHAgMBAAECggEAUz29cXKDbjqJLgoeizlgMQosRgvrYW5f98i8Fg7wj3et\nj+fUjRCg5BO5o6Mdl4twi0FN0C/H6Ur0Oom5mnZ+Zq8YCxWterOM9TYSPZP51c5i\nwQxq6WAKa4zhtkhWh+HwjiSvmUewYHvxfVb9zaVv7PWnkeEROY9De9eNBpk7e9lK\nINhXH06h73snqOWfQOa3vvGnvQY2d1FikIl9xVgbB2JyiyMWnQqWOiua4ES1qGvg\nIW2ZdrxkMOltVZVIFcyEN1N+Yq7uSOS2AdcJvgS5tSDEQGYsk1myPvh40rbSwE65\nMu8CEmBtiahPj2yURtjr1QJRMwfpqKMEA1vs+rkPKQKBgQDBlZVOa79YpjFypb/E\nZuyWOzxbQPe4vuN4Xg6/L22SQQyQcPXOmudOq5PInzyltUS/O2or69JO0elc2e4O\nbi9p0qcTAFRFwS3MXPKoWhJx7qz6kBaAngvsvazBoEPbVTul9jf5N4yZ1PIKWvZ1\ncNsbW69CDFcJa0FhpqdEZadxqwKBgQD1E30BcmdCuY0hZvY8Ys4AjCcOo39n3v9i\nCZfu10sNu6G6HFcA1vl/0w3mt2Zhe/AVKkI4tMBFrwAcWOWgnJHcfttMBA0IhSOb\nlifJtNAtdpD8YyESm4sDJkN9NRiIcjl5SfbQTqfv2jTUS2IHwErSpA2IGUPxMmpv\nndQW+IntlQKBgQCWhghNrQxhEwEaA3XEcr995Vt+HVtBxPQ88O4IjQlnEruBSMRp\nPRukmVdVRTQ0KWnmRH2+3yRtc11AfJhVkim94DVXWgctIhPJd3CbpUX4Xz0Tq5xD\nsCDXmXjOKh75WiICXxyQ5TenmeQGV0qScQl+EWGKOuwmf8ab9qjTTzPO9wKBgDtv\njIOLUUu3YGjXZaRjef5b/yTntgBk7p7CINzFzef4t1JBiUAk4sGDV/26QNQnkKQx\naXoDLBY6SNaXIKmBD0bgZSLcYKTOVUJoeiHK/inodWUgTWcL3fzXjMqS0+5TdWja\n7Ua7rLLYwrMxzQaANiU7sCRvrDCUv4duSX+Aq0K5AoGBAL5TWCYZy4tqzybQ4SJ4\n9ZkK/ifDBlsjvAzaAfQ+I0ezZGpwAWhp0cRQEnso+fTEAE9cbBwrapejmy0HRCEn\nu1ah8an+C855EZ/3/VLVlFru5oqTpFwcEf/UZjux3tipPlNCoGCvdSr7HKaIWeu+\nUNYIGo5P6qVxBov76JbEErvD\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "integration/goldens/rootCA.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEnjCCAwagAwIBAgIRAOPa8EEi4WZW/wxKT9ad82AwDQYJKoZIhvcNAQELBQAw\nZzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMR4wHAYDVQQLDBV2YWdy\nYW50QHVidW50dS1pbXBpc2gxJTAjBgNVBAMMHG1rY2VydCB2YWdyYW50QHVidW50\ndS1pbXBpc2gwHhcNMjIwNTE3MjAxNjQwWhcNMzIwNTE3MjAxNjQwWjBnMR4wHAYD\nVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExHjAcBgNVBAsMFXZhZ3JhbnRAdWJ1\nbnR1LWltcGlzaDElMCMGA1UEAwwcbWtjZXJ0IHZhZ3JhbnRAdWJ1bnR1LWltcGlz\naDCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAPIb03qvQ7kn/1UzKLC/\nwFYC8I7l5Aqfk3HyzFsOcWAh2NuyEhWq2TGhc88PXMIh1qtG6Np84BHbBdOjJNol\n5QaNF/olelGykEEcdedPrtiH52rlgRdVlaeal/DbnzoNdJfArFPme0rzBDwSQkRi\nND+mVjXObNKcH9tc8Jb3SOCQ0HHSrmltetZPgSCM1msXUJneVuJWeAr632OVXrZa\n1AEdjf22xd1LePEvOvsmbHMvw/XgLDEMyLQmzdXGmwAhzgKzwGkvQYxvilqvJB31\nxm+ws81mVpbOq/huMnxAMoZjvxOm5RdrJF1BZmrlNDtTGBlA0dhTa1xFvxuC9x19\nSXoAzc66gn/wsDx3ipKqzyGgeoig6ZVAZtwofv9BozEE3cnZ7ovsvG+a9Qyvr4KH\n2KLjjynV1sLRhhwGDawqaYXJRK/2MMaFZ29/UyuKMr4c5fYZncZAn5EgXvvUJaVe\nzG4SVw4mGciDIUfFcoYqbquEJA5XOORlK2y4zCR91yKg0wIDAQABo0UwQzAOBgNV\nHQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUG4YzImTE\n+mXy7sUlfgRasp1AX0wwDQYJKoZIhvcNAQELBQADggGBAMwqGJMVD8FOsDh1Bfa5\ndCZ/ybtUgPTcI4Pky98kilhlV1bPeacW6SD4W3Yf7sH1kVKSQgFS88nxLpcczGiw\nHO7gpgRv8QkoWGlIwdAlIvW/BAf8vByvpXBl1kd6anOAnwfwndxgUCc7aqZE/4xE\nvD0TJ+cbg+w81pmYwayouVnfHRpK5wKsGp2erEaGZceoZIfdHqNE3ecGV4r3izEC\n3CgPTaLcGwkKkP18j5pOaL034nnxZV8uM0pwitHUMubMMAXiO86CayYzroKElTEa\nm6pHIOOBc/e3kkUdiJYLDeM4zlNIvZrdFSg2R6AKeIjMF1VOeW+5kvc0rmpbkeaI\niIXoBkwzad/jab9XxhEqFK6/JL9VOH2Yf36F47sAu9vSRZbhWXNVUzvL7ktIpfHL\n6COqoVOttkZj8yRyWje3M/RCnk6gS1BKxSDhO85kAy5zbNk1khgOO4W+ur4tFatv\n+iGSbAcOFDwb6Ygnp9+Y/ZhMARZHJfDg39/BhQIYQ1cCNw==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "integration/goldens/server-cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEHjCCAoagAwIBAgIQKa6srh/okxC7U4sW60pAvzANBgkqhkiG9w0BAQsFADBn\nMR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExHjAcBgNVBAsMFXZhZ3Jh\nbnRAdWJ1bnR1LWltcGlzaDElMCMGA1UEAwwcbWtjZXJ0IHZhZ3JhbnRAdWJ1bnR1\nLWltcGlzaDAeFw0yMjA1MTcyMTAzNDlaFw0yNDA4MTcyMTAzNDlaMEkxJzAlBgNV\nBAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEeMBwGA1UECwwVdmFn\ncmFudEB1YnVudHUtaW1waXNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\nAQEAmwLSqDxB+9/JipYDzUoE88aMo1B7O3fUgg7pOQS7m1YVwnB9YDrrisyd4Khk\n5f3hUbf9tly1OjaVgDfs6khT2QVjs4fxXRYNF9+e1W53EyXokpqGybhrfkWHPdYw\nePQrx6wn4+Fa5qm7BrIltWEKNMKAg0esTrPjnWR7MHZoJ4tAd00o0ymPxtXTAvXY\nqpc/nYDE8823jqhW182wu3zVwyZGBlvy9MrPMCA4X+3nT6nyPlAl77E87Nztahch\n+aiGn7K0Q+Yfqn7vf7p1g11syYThUHSqSRQUMrBObeMXp/9RErlbza5fm9VM30RG\nLD2beMvTkKDWd5/nxzvyMKtAKQIDAQABo2QwYjAOBgNVHQ8BAf8EBAMCBaAwEwYD\nVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUG4YzImTE+mXy7sUlfgRasp1A\nX0wwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwQAAAAAMA0GCSqGSIb3DQEBCwUAA4IB\ngQBqXvYt4m95MAA7Zy+0AJUCbJTUC5W8Kq2e5dvxCAETcgBsw2Aroob5tilhALgS\n8JK3MnwUvyti+qyP8TmAVp4tu3VyZw0ME5B0ArTSHwuuvSZv59OYX85ioJLsGDuV\nTET2d6VMH5NpIe70DYtoib1NvzLcBxyR8olrz0Xc6tOwWwoxc8lPakdwBcVTPnye\nIh2fBla+7h28CSniy3xVpcdum0zkKtlTp2Rf6chHAcS7SMrwSAjRccFDqCm+XhEA\nbSqJC7xyKxgJ6SBDoX02jFQXmsE0i0UoGQCKztB+JJZaxMPs9KojK3dBKdEUtAbO\nNTEg66a36wTogb7EtCv/qShFXsotm+xWXoCHFms/8/pbVrYbSVrrAPN4d6ctWMNT\n6Pp5a+vBh45bHZaGRypspdK+cpJp8i90fxx/fccqh+y9CjlOIuaI86V2agUfPM3r\n5K2Vt6A2fmmLH8iuUzfTeMc5+/VGrhXW3toMuYfVDe1t6owmHpc9PjQBxXsAKwS7\nNEA=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "integration/goldens/server-key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCbAtKoPEH738mK\nlgPNSgTzxoyjUHs7d9SCDuk5BLubVhXCcH1gOuuKzJ3gqGTl/eFRt/22XLU6NpWA\nN+zqSFPZBWOzh/FdFg0X357VbncTJeiSmobJuGt+RYc91jB49CvHrCfj4VrmqbsG\nsiW1YQo0woCDR6xOs+OdZHswdmgni0B3TSjTKY/G1dMC9diqlz+dgMTzzbeOqFbX\nzbC7fNXDJkYGW/L0ys8wIDhf7edPqfI+UCXvsTzs3O1qFyH5qIafsrRD5h+qfu9/\nunWDXWzJhOFQdKpJFBQysE5t4xen/1ESuVvNrl+b1UzfREYsPZt4y9OQoNZ3n+fH\nO/Iwq0ApAgMBAAECggEAAXsLbBP3RWKj2DEKpEFavsFcwoqYQr8oRn3B+It1B0zR\nNGRCrlKhwVenAmhZXmEIb/Fj+QP1rzKqRulB2BMMyfvlMefQhGUbhujZ6Yv+ky4/\nhHb5Zp/eVvxvCEYtfFpIuMWKNAEJDz77zFqTRaJBqLNa6uYaXT4WpIA7NQFMiWQl\nUSFGqTyv/v/xWrPEf59vgglmlVBHHkn3iEyl//zlykSBojV9mGNVLwGT6pQzmyA+\nU1dwb+f6VXWPT6X0d18EybjgNbS7KDh08o2m3TdHIn+GroP9Fz0y4XnnML6B7jOr\nHSoiXVmd53xc0lnvN+ZGVgbxOidqW6ZzKs74g24XyQKBgQDKanyjmX50QY54fVk1\nb31Cgfy8TK7kn3ZxKie5GMbrjB1IjKgkBcaDH0oUICNquAKQuZf1vvnilIkg1wMz\nB7zYRIioNOvYTUKNI8RRy+opT9/GjE0pCnOmCVS6JMWH+JJSJ+8lK8+EAcg2CDDc\nJ6GqH0VFDgpMgFC0pTD/6aQYSwKBgQDEC8EQ3/d8KOHKTywYcEqZimCcZiek5pJ7\nPldTzA5WhJ0RoVG9zr1Q1aMOGh6nZdL0FzfeqzrvbnQzIoJNRzEjAvgu/WrLeTO6\nR0TlM7Yv7/gMBvKAvhlt4rLSLj+jBklJ0o+EivonNvom5/E9GhTk9ThYFLpRGIM1\n49ObasRo2wKBgCqHmUyeka1HoPG8FKRfmogA2+OMkSrEPpSkcNg5VoJjXDvEIgf/\nlqgySNRVtFtV8mQKOt2NlN+AqhdWVfvA4nLmY3hENlGqWhNhsnwlRjwL5k7HZ+b5\npTb7uuupkj9xgPz2Gw9KVz27Sh/gNLPtN9CQ5XMO8icExvBNzS/YHXxlAoGAANNb\n2MpYrHkvmj6DHQc8CTF//qifeh5Od55nl77AQLUGRFC9m2MHgROlQCX0Axh2rvh6\nD8IA2YlJi/2VNuYZ2t/kU65ViM3V3e7251A0cDc+gC53Tvt99+yAfEodkn4wSNNO\nL2y2aPveSdOoPG2jsOMnfZd4wWatBskS1xC4CwkCgYARzyoxf2O+WnNrkE6B7Uc1\nhyV8bqO5TQNjHG/l4lur/71GWBguEzNwGyyI2HS9xeK+95/1Y/euDXyFUvBVWjFT\nSVt1d/TVzUv6WxFaEHIhIjmPFhbzBNwwz9ixIOf3uTeifptaw+tjhLCwVQhMErgf\nDj8U2KJri5KKNn6wBSwU6w==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "integration/goldens/simple.yaml",
    "content": "name: Test\n\nroutes:\n- name: terminate-and-notify\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  actions: [terminate-pod, send-slack-message]\n\nactions:\n- name: stdout\n  type: stdout\n  enable: true\n\n- name: terminate-pod\n  runs-on: \"test-runner-1\"\n  type: exec\n  enable: true\n  exec-script: |\n    #!/bin/sh\n    PID=$(echo $POSTEE_EVENT | jq -r .Context.hostName)\n    kubectl delete pod $PID     # If terminating a K8s pod\n    # pkill -SIGTERM $PID       # If terminating a UNIX process\n\n- name: send-slack-message\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/TAAAA/BBB/<key>\n"
  },
  {
    "path": "integration/goldens/test-seed.txt",
    "content": "SUAGAA3TNI36JHTD6GLFJRR6KZIY7YXS2ZISHQA4LPZZZG2D6KG5JPV7DM\nUBUQ63VFZEW3IS7RGQQZF5DIT2FTCMTZAAHFENK3G5M6ADRZ5WAJLAQN\n"
  },
  {
    "path": "layout/assurances.go",
    "content": "package layout\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc RenderAssurances(provider LayoutProvider, assuranceResults data.ImageAssuranceResults) string {\n\tvar assurances [][]string\n\tassurances = append(assurances, []string{\"#\", \"Control\", \"Policy Name\", \"Status\"})\n\n\tfor i, ass := range assuranceResults.ChecksPerformed {\n\t\tvar status string\n\t\tif ass.Failed {\n\t\t\tstatus = \"FAIL\"\n\t\t} else {\n\t\t\tstatus = \"PASS\"\n\t\t}\n\t\tassurances = append(assurances, []string{\n\t\t\tstrconv.Itoa(i + 1),\n\t\t\tass.Control,\n\t\t\tass.PolicyName,\n\t\t\tstatus,\n\t\t})\n\t}\n\treturn provider.Table(assurances)\n}\n"
  },
  {
    "path": "layout/colors.go",
    "content": "package layout\n\nfunc CriticalColor() string   { return \"#c00000\" }\nfunc HighColor() string       { return \"#e0443d\" }\nfunc MediumColor() string     { return \"#f79421\" }\nfunc LowColor() string        { return \"#e1c930\" }\nfunc NegligibleColor() string { return \"green\" }\n"
  },
  {
    "path": "layout/malware.go",
    "content": "package layout\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc RenderMalware(malware []data.MalwareData, provider LayoutProvider, builder *bytes.Buffer) {\n\tvar table [][]string\n\ttable = append(table, []string{\"#\", \"Malware\", \"Hash\", \"Path\"})\n\n\tfor i, malware := range malware {\n\t\ttable = append(table, []string{strconv.Itoa(i), malware.Malware, malware.Hash, malware.Path})\n\t}\n\tbuilder.WriteString(provider.Table(table))\n}\n"
  },
  {
    "path": "layout/provider.go",
    "content": "package layout\n\ntype LayoutProvider interface {\n\tTitleH1(title string) string\n\tTitleH2(title string) string\n\tTitleH3(title string) string\n\tColourText(text, color string) string\n\tTable(rows [][]string) string\n\tP(p string) string\n\tA(url, title string) string\n}\n"
  },
  {
    "path": "layout/sensitive.go",
    "content": "package layout\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc RenderSensitiveData(sensitive []data.SensitiveData, provider LayoutProvider, builder *bytes.Buffer) {\n\tvar table [][]string\n\ttable = append(table, []string{\"File name\", \"Path\", \"Type\", \"Hash\"})\n\n\tfor _, s := range sensitive {\n\t\ttable = append(table, []string{s.Filename, s.Path, s.Type, s.Hash})\n\t}\n\tbuilder.WriteString(provider.Table(table))\n}\n"
  },
  {
    "path": "layout/ticketLayout.go",
    "content": "package layout\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc GenTestDescription(provider LayoutProvider, raw string) string {\n\tvar builder bytes.Buffer\n\tbuilder.WriteString(provider.P(raw))\n\n\treturn builder.String()\n}\nfunc GenTicketDescription(provider LayoutProvider, scanInfo, prevScan *data.ScanImageInfo, serverUrl, image_url_part string) string {\n\tvar builder bytes.Buffer\n\tbuilder.WriteString(provider.P(\"Image name: \" + scanInfo.Image))\n\tbuilder.WriteString(provider.P(\"Registry: \" + scanInfo.Registry))\n\tif scanInfo.Disallowed {\n\t\tbuilder.WriteString(provider.P(\"Image is non-compliant\"))\n\t} else {\n\t\tbuilder.WriteString(provider.P(\"Image is compliant\"))\n\t}\n\n\tif scanInfo.ScanMalware {\n\t\tif scanInfo.Malware > 0 {\n\t\t\tbuilder.WriteString(provider.P(\"Malware found: Yes\"))\n\t\t} else {\n\t\t\tbuilder.WriteString(provider.P(\"Malware found: No\"))\n\t\t}\n\t}\n\n\tif scanInfo.ScanSensitiveData {\n\t\tif scanInfo.Sensitive > 0 {\n\t\t\tbuilder.WriteString(provider.P(\"Sensitive data found: Yes\"))\n\t\t} else {\n\t\t\tbuilder.WriteString(provider.P(\"Sensitive data found: No\"))\n\t\t}\n\t}\n\n\tbuilder.WriteString(VulnerabilitiesTable(provider, [2][]string{\n\t\t{\"CRITICAL\", \"HIGH\", \"MEDIUM\", \"LOW\", \"NEGLIGIBLE\"},\n\t\t{strconv.Itoa(scanInfo.Critical), strconv.Itoa(scanInfo.High), strconv.Itoa(scanInfo.Medium), strconv.Itoa(scanInfo.Low), strconv.Itoa(scanInfo.Negligible)},\n\t}))\n\n\t// Rendering Assurances\n\tif len(scanInfo.ImageAssuranceResults.ChecksPerformed) > 0 {\n\t\tbuilder.WriteString(provider.TitleH2(\"Assurance controls\"))\n\t\tbuilder.WriteString(RenderAssurances(provider, scanInfo.ImageAssuranceResults))\n\t}\n\n\t// Rendering Found vulnerabilities\n\tif len(scanInfo.Resources) > 0 {\n\t\tbuilder.WriteString(provider.TitleH2(\"Found vulnerabilities\"))\n\t\tRenderVulnerabilities(scanInfo.Resources, provider, &builder)\n\t}\n\n\t// Discovered vulnerabilities from last scan:\n\tif prevScan != nil && len(prevScan.Resources) > 0 {\n\t\tbuilder.WriteString(\"\\n\")\n\t\tbuilder.WriteString(provider.TitleH2(\"Discovered vulnerabilities from last scan\"))\n\t\tRenderVulnerabilities(prevScan.Resources, provider, &builder)\n\t}\n\tif len(scanInfo.Malwares) > 0 {\n\t\tbuilder.WriteString(\"\\n\")\n\t\tbuilder.WriteString(provider.TitleH2(\"Malware\"))\n\t\tRenderMalware(scanInfo.Malwares, provider, &builder)\n\t}\n\tif len(scanInfo.SensitiveData) > 0 {\n\t\tbuilder.WriteString(\"\\n\")\n\t\tbuilder.WriteString(provider.TitleH2(\"Sensitive Data\"))\n\t\tRenderSensitiveData(scanInfo.SensitiveData, provider, &builder)\n\t}\n\t// Checked that the aqua-server name is not empty\n\tif len(serverUrl) > 0 && len(image_url_part) > 0 {\n\t\tbuilder.WriteString(provider.P(\"See more: \" + provider.A(serverUrl+image_url_part, serverUrl+image_url_part)))\n\t}\n\treturn builder.String()\n}\n"
  },
  {
    "path": "layout/vulnerabilities.go",
    "content": "package layout\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nconst empty = \"none\"\n\nfunc RenderVulnerabilities(resources []data.InfoResources, provider LayoutProvider, builder *bytes.Buffer) {\n\trating := make(map[string][][]string)\n\tfor _, r := range resources {\n\t\tvar resourceName, installedVersion string\n\t\tif r.ResourceDetails.Name == \"\" {\n\t\t\tresourceName = empty\n\t\t} else {\n\t\t\tresourceName = r.ResourceDetails.Name\n\t\t}\n\t\tif r.ResourceDetails.Version == \"\" {\n\t\t\tinstalledVersion = empty\n\t\t} else {\n\t\t\tinstalledVersion = r.ResourceDetails.Version\n\t\t}\n\t\tfor _, v := range r.Vulnerabilities {\n\t\t\tvar vulnerabilityId, fixVersion string\n\t\t\tif v.Name == \"\" {\n\t\t\t\tvulnerabilityId = empty\n\t\t\t} else {\n\t\t\t\tvulnerabilityId = v.Name\n\t\t\t}\n\t\t\tif v.FixVersion == \"\" {\n\t\t\t\tfixVersion = empty\n\t\t\t} else {\n\t\t\t\tfixVersion = data.ClearField(v.FixVersion)\n\t\t\t}\n\t\t\tkey := strings.ToLower(v.Severity)\n\t\t\trating[key] = append(rating[key], []string{vulnerabilityId, resourceName, installedVersion, fixVersion})\n\t\t}\n\t}\n\torder := [...]string{\"critical\", \"high\", \"medium\", \"low\", \"negligible\"}\n\tfor _, title := range order {\n\t\tvulnerabilities, ok := rating[title]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tbuilder.WriteString(provider.TitleH3(strings.Title(title) + \" severity vulnerabilities\"))\n\t\tvar table [][]string\n\t\ttable = append(table, []string{\"Vulnerability ID\", \"Resource name\", \"Installed version\", \"Fix version\"})\n\t\ttable = append(table, vulnerabilities...)\n\t\tbuilder.WriteString(provider.Table(table))\n\t}\n}\n\nfunc VulnerabilitiesTable(provider LayoutProvider, rows [2][]string) string {\n\tif len(rows) != 2 && len(rows[1]) != 5 {\n\t\treturn \"\"\n\t}\n\tvar table [][]string\n\ttable = append(table, rows[0])\n\tvar r []string\n\tr = append(r, provider.ColourText(rows[1][0], CriticalColor()))\n\tr = append(r, provider.ColourText(rows[1][1], HighColor()))\n\tr = append(r, provider.ColourText(rows[1][2], MediumColor()))\n\tr = append(r, provider.ColourText(rows[1][3], LowColor()))\n\tr = append(r, provider.ColourText(rows[1][4], NegligibleColor()))\n\ttable = append(table, r)\n\treturn provider.Table(table)\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"syscall\"\n\n\t\"github.com/aquasecurity/postee/v2/controller\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/router\"\n\t\"github.com/aquasecurity/postee/v2/runner\"\n\t\"github.com/aquasecurity/postee/v2/utils\"\n\t\"github.com/aquasecurity/postee/v2/webserver\"\n\t\"github.com/spf13/cobra\"\n)\n\nconst (\n\tURL       = \"0.0.0.0:8082\"\n\tTLS       = \"0.0.0.0:8445\"\n\tURL_USAGE = \"The socket to bind to, specified using host:port.\"\n\tTLS_USAGE = \"The TLS socket to bind to, specified using host:port.\"\n\tCFG_FILE  = \"/config/cfg.yaml\"\n\tCFG_USAGE = \"The alert configuration file.\"\n)\n\nvar (\n\turl            = \"\"\n\ttls            = \"\"\n\tcfgfile        = \"\"\n\tcontrollerMode = false\n\n\tcontrollerURL          = \"\"\n\tcontrollerCARootPath   = \"\"\n\tcontrollerTLSCertPath  = \"\"\n\tcontrollerTLSKeyPath   = \"\"\n\tcontrollerSeedFilePath = \"\"\n\trunnerSeedFilePath     = \"\"\n\n\trunnerName        = \"\"\n\trunnerCARootPath  = \"\"\n\trunnerTLSCertPath = \"\"\n\trunnerTLSKeyPath  = \"\"\n)\n\nvar rootCmd = &cobra.Command{\n\tUse:   \"webhooksrv\",\n\tShort: fmt.Sprintf(\"Aqua Container Security Webhook server\\n\"),\n\tLong:  fmt.Sprintf(\"Aqua Container Security Webhook server\\n\"),\n}\n\nfunc init() {\n\trootCmd.Flags().StringVar(&url, \"url\", URL, URL_USAGE)\n\trootCmd.Flags().StringVar(&tls, \"tls\", TLS, TLS_USAGE)\n\trootCmd.Flags().StringVar(&cfgfile, \"cfgfile\", CFG_FILE, CFG_USAGE)\n\n\trootCmd.Flags().BoolVar(&controllerMode, \"controller-mode\", false, \"run postee in controller mode\")\n\trootCmd.Flags().StringVar(&controllerURL, \"controller-url\", \"\", \"postee controller URL\")\n\trootCmd.Flags().StringVar(&controllerCARootPath, \"controller-ca-root\", \"\", \"postee controller ca root file\")\n\trootCmd.Flags().StringVar(&controllerTLSCertPath, \"controller-tls-cert\", \"\", \"postee controller TLS cert file\")\n\trootCmd.Flags().StringVar(&controllerTLSKeyPath, \"controller-tls-key\", \"\", \"postee controller TLS key file\")\n\trootCmd.Flags().StringVar(&controllerSeedFilePath, \"controller-seed-file\", \"\", \"postee controller AuthN seed file\")\n\n\trootCmd.Flags().StringVar(&runnerName, \"runner-name\", \"\", \"postee runner name\")\n\trootCmd.Flags().StringVar(&runnerCARootPath, \"runner-ca-root\", \"\", \"postee runner ca root file\")\n\trootCmd.Flags().StringVar(&runnerTLSCertPath, \"runner-tls-cert\", \"\", \"postee runner tls cert file\")\n\trootCmd.Flags().StringVar(&runnerTLSKeyPath, \"runner-tls-key\", \"\", \"postee runner tls key file\")\n\trootCmd.Flags().StringVar(&runnerSeedFilePath, \"runner-seed-file\", \"\", \"postee runner AuthN seed file\")\n}\n\nfunc main() {\n\truntime.GOMAXPROCS(runtime.NumCPU())\n\tutils.InitDebug()\n\n\trootCmd.Run = func(cmd *cobra.Command, args []string) {\n\t\trtr := router.Instance()\n\n\t\tif runnerName != \"\" {\n\t\t\tif controllerMode {\n\t\t\t\tlog.Fatal(\"postee cannot run as a controller when running in runner mode\")\n\t\t\t}\n\n\t\t\tf, err := ioutil.TempFile(\"\", \"temp-postee-config-*\") // TODO: Find a better way\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatal(\"Unable to create temp file for runner config on disk: \", err)\n\t\t\t}\n\n\t\t\trnr := runner.Runner{\n\t\t\t\tControllerURL:      controllerURL,\n\t\t\t\tRunnerSeedFilePath: runnerSeedFilePath,\n\t\t\t\tRunnerCARootPath:   runnerCARootPath,\n\t\t\t\tRunnerTLSKeyPath:   runnerTLSKeyPath,\n\t\t\t\tRunnerTLSCertPath:  runnerTLSCertPath,\n\t\t\t\tRunnerName:         runnerName,\n\t\t\t}\n\t\t\tif err := rnr.Setup(rtr, f); err != nil {\n\t\t\t\tlog.Fatal(\"Failed to launch runner: \", err)\n\t\t\t}\n\t\t\tdefer func() { os.Remove(f.Name()) }()\n\n\t\t\tcfgfile = f.Name()\n\t\t}\n\n\t\tif controllerMode {\n\t\t\tif runnerName != \"\" {\n\t\t\t\tlog.Fatal(\"postee cannot run as a runner when running in controller mode\")\n\t\t\t}\n\n\t\t\tctr := controller.Controller{\n\t\t\t\tControllerURL:          controllerURL,\n\t\t\t\tControllerSeedFilePath: controllerSeedFilePath,\n\t\t\t\tControllerCAFile:       controllerCARootPath,\n\t\t\t\tControllerTLSKeyPath:   controllerTLSKeyPath,\n\t\t\t\tControllerTLSCertPath:  controllerTLSCertPath,\n\t\t\t\tRunnerName:             runnerName,\n\t\t\t}\n\t\t\tif err := ctr.Setup(rtr); err != nil {\n\t\t\t\tlog.Fatal(\"Failed to launch controller: \", err)\n\t\t\t}\n\t\t}\n\n\t\tif os.Getenv(\"AQUAALERT_URL\") != \"\" {\n\t\t\turl = os.Getenv(\"AQUAALERT_URL\")\n\t\t}\n\n\t\tif os.Getenv(\"POSTEE_HTTP\") != \"\" {\n\t\t\turl = os.Getenv(\"POSTEE_HTTP\")\n\t\t}\n\n\t\tif os.Getenv(\"AQUAALERT_TLS\") != \"\" {\n\t\t\ttls = os.Getenv(\"AQUAALERT_TLS\")\n\t\t}\n\n\t\tif os.Getenv(\"POSTEE_HTTPS\") != \"\" {\n\t\t\ttls = os.Getenv(\"POSTEE_HTTPS\")\n\t\t}\n\n\t\tif os.Getenv(\"AQUAALERT_CFG\") != \"\" {\n\t\t\tcfgfile = os.Getenv(\"AQUAALERT_CFG\")\n\t\t}\n\n\t\tif os.Getenv(\"POSTEE_CFG\") != \"\" {\n\t\t\tcfgfile = os.Getenv(\"POSTEE_CFG\")\n\t\t}\n\n\t\tif os.Getenv(\"PATH_TO_DB\") != \"\" {\n\t\t\tdbservice.SetNewDbPathFromEnv()\n\t\t}\n\n\t\terr := rtr.Start(cfgfile)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Can't start alert manager %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tdefer rtr.Terminate()\n\n\t\tgo webserver.Instance().Start(url, tls)\n\t\tdefer webserver.Instance().Terminate()\n\n\t\tDaemonize()\n\t}\n\terr := rootCmd.Execute()\n\tif err != nil {\n\t\tlog.Printf(\"Can't start command %v\", err)\n\t\treturn\n\t}\n}\n\nfunc Daemonize() {\n\tsigs := make(chan os.Signal, 1)\n\tdone := make(chan bool, 1)\n\tsignal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)\n\n\tgo func() {\n\t\tsig := <-sigs\n\t\tlog.Println(sig)\n\t\tdone <- true\n\t}()\n\n\t<-done\n}\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: Postee\nsite_url: https://aquasecurity.github.io/postee\nsite_description: Integrate vulnerability scanning with a variety of systems.\ndocs_dir: docs/\nrepo_name: Postee\nrepo_url: https://github.com/aquasecurity/postee\nedit_uri: \"\"\ncopyright: Copyright 2019-2022 Aqua Security Software Ltd.\n\nnav:\n- Introduction: index.md\n- Use cases:\n  - Simple Examples: examples.md\n  - Blueprints:\n    - Trivy Scan: blueprints/trivy-vulnerability-scan.md\n    - Trivy AWS Security Hub: blueprints/trivy-aws-security-hub.md\n    - Trivy Operator: blueprints/trivy-operator.md\n    - Pagerduty: blueprints/devops-pagerduty.md\n    - Others:\n      - External Healthcheck: blueprints/external-healthcheck.md\n      - Image Processing: blueprints/image-processing.md\n- Installation: install.md\n- Configuration:\n  - Config File: config.md\n  - General: settings.md\n  - Routes: routes.md\n  - Templates: templates.md\n  - Actions: actions.md\n  - Controller/Runner: controller-runner.md\n- Demo: demo.md\n- Aqua Cloud: aquacloud.md\n- Postee UI: ui.md\n- Advanced: advanced.md\n\ntheme:\n  name: material\n  custom_dir: overrides\n  language: 'en'\n  logo: img/postee.png\n  palette:\n    - media: \"(prefers-color-scheme: light)\"\n      scheme: default\n      toggle:\n        icon: material/toggle-switch-off-outline\n        name: Switch to dark mode\n    - media: \"(prefers-color-scheme: dark)\"\n      scheme: slate\n      toggle:\n        icon: material/toggle-switch\n        name: Switch to light mode\n\n\nmarkdown_extensions:\n- pymdownx.highlight\n- pymdownx.details\n- pymdownx.superfences\n- admonition\n- attr_list\n- md_in_html\n- toc:\n    permalink: true\n\n# All data defined under extra is automatically exposed as a variable and can\n# be used from the template. For example, {{ var.version }}.\n#\n# Requires pip install mike and pip install git+https://${GH_TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git\nextra:\n  generator: false\n  social:\n  - icon: fontawesome/brands/github\n    link: https://github.com/aquasecurity\n  - icon: fontawesome/brands/slack\n    link: https://slack.aquasec.com/\n  - icon: fontawesome/brands/youtube\n    link: https://www.youtube.com/channel/UCZd5NF4XJRaU-yfextsY-pw\n  - icon: fontawesome/brands/twitter\n    link: https://twitter.com/AquaSecTeam\n  - icon: fontawesome/brands/linkedin\n    link: https://www.linkedin.com/company/aquasecteam/\n  version:\n    provider: mike\n\n# Requires pip install mkdocs-macros-plugin\nplugins:\n- search\n- macros\n"
  },
  {
    "path": "msgservice/aggregatebytime_test.go",
    "content": "package msgservice\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n)\n\nfunc TestAggregateByTimeout(t *testing.T) {\n\tconst aggregationSeconds = 3\n\n\tdbPathReal := dbservice.DbPath\n\tsavedRunScheduler := RunScheduler\n\tschedulerInvctCnt := 0\n\tdefer func() {\n\t\tos.Remove(dbservice.DbPath)\n\t\tdbservice.ChangeDbPath(dbPathReal)\n\t\tRunScheduler = savedRunScheduler\n\t}()\n\tRunScheduler = func(\n\t\troute *routes.InputRoute,\n\t\tfnSend func(plg actions.Action, cnt map[string]string),\n\t\tfnAggregate func(outputName string, currentContent map[string]string, counts int, ignoreLength bool) []map[string]string,\n\t\tinpteval data.Inpteval,\n\t\tname *string,\n\t\toutput actions.Action,\n\t) {\n\t\tlog.Printf(\"Mocked Scheduler is activated for route %q. Period: %d sec\", route.Name, route.Plugins.AggregateTimeoutSeconds)\n\t\troute.StartScheduler()\n\n\t\tschedulerInvctCnt++\n\t}\n\n\tdbservice.ChangeDbPath(\"test_webhooks.db\")\n\tdbservice.DbPath = \"test_webhooks.db\"\n\n\tdemoRoute := &routes.InputRoute{\n\t\tName: \"demo-route1\",\n\t\tPlugins: routes.Plugins{\n\t\t\tAggregateTimeoutSeconds: aggregationSeconds,\n\t\t},\n\t}\n\n\tdemoEmailPlg := &DemoEmailAction{}\n\n\tdemoInptEval := &DemoInptEval{}\n\n\tsrvUrl := \"\"\n\n\tsrv1 := new(MsgService)\n\tsrv1.MsgHandling([]byte(mockScan1), demoEmailPlg, demoRoute, demoInptEval, &srvUrl)\n\tsrv1.MsgHandling([]byte(mockScan2), demoEmailPlg, demoRoute, demoInptEval, &srvUrl)\n\tsrv1.MsgHandling([]byte(mockScan3), demoEmailPlg, demoRoute, demoInptEval, &srvUrl)\n\n\texpectedSchedulerInvctCnt := 1\n\n\tif schedulerInvctCnt != expectedSchedulerInvctCnt {\n\t\tt.Errorf(\"Unexpected plugin invocation count %d, expected %d \\n\", schedulerInvctCnt, expectedSchedulerInvctCnt)\n\t}\n\n\tdemoRoute.StopScheduler()\n}\n"
  },
  {
    "path": "msgservice/aggregatescan_test.go",
    "content": "package msgservice\n\nimport (\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n)\n\nfunc TestAggregateIssuesPerTicket(t *testing.T) {\n\ttests := []struct {\n\t\tcaseDesc              string\n\t\texpectedSntCnt        int\n\t\texpectedRenderCnt     int\n\t\texpectedAggrRenderCnt int\n\t\tskipAggrSpprt         bool\n\t}{\n\t\t{\n\t\t\tcaseDesc:              \"basic\",\n\t\t\texpectedSntCnt:        1,\n\t\t\texpectedRenderCnt:     4,\n\t\t\texpectedAggrRenderCnt: 1,\n\t\t},\n\t\t{\n\t\t\tcaseDesc:              \"no aggregation supported\",\n\t\t\texpectedSntCnt:        4,\n\t\t\texpectedRenderCnt:     4,\n\t\t\texpectedAggrRenderCnt: 0,\n\t\t\tskipAggrSpprt:         true,\n\t\t},\n\t}\n\n\tdbPathReal := dbservice.DbPath\n\tdefer func() {\n\t\tos.Remove(dbservice.DbPath)\n\t\tdbservice.ChangeDbPath(dbPathReal)\n\t}()\n\tdbservice.ChangeDbPath(\"test_webhooks.db\")\n\n\tfor _, test := range tests {\n\t\tdoAggregate(t, test.caseDesc, test.expectedSntCnt, test.expectedRenderCnt, test.expectedAggrRenderCnt, test.skipAggrSpprt)\n\t}\n\n}\nfunc doAggregate(t *testing.T, caseDesc string, expectedSntCnt int, expectedRenderCnt int, expectedAggrRenderCnt int, skipAggrSpprt bool) {\n\tdemoEmailAction := &DemoEmailAction{\n\t\temailCounts: 0,\n\t}\n\n\tscans := []string{mockScan1, mockScan2, mockScan3, mockScan4}\n\n\tsrvUrl := \"\"\n\tdemoRoute := &routes.InputRoute{}\n\n\tdemoRoute.Name = \"demo-route\"\n\tdemoRoute.Plugins.AggregateMessageNumber = 3\n\n\tdemoInptEval := &DemoInptEval{\n\t\tskipAggrSpprt: skipAggrSpprt,\n\t}\n\n\tdemoEmailAction.wg = &sync.WaitGroup{}\n\tdemoEmailAction.wg.Add(expectedSntCnt)\n\n\tfor _, scan := range scans {\n\t\tsrv := new(MsgService)\n\t\tsrv.MsgHandling([]byte(scan), demoEmailAction, demoRoute, demoInptEval, &srvUrl)\n\t}\n\n\tdemoEmailAction.wg.Wait()\n\n\tif demoEmailAction.getEmailsCount() != expectedSntCnt {\n\t\tt.Errorf(\"%s: The number of sent email doesn't match expected value. Sent: %d, expected: %d \", caseDesc, demoEmailAction.getEmailsCount(), expectedSntCnt)\n\t}\n\n\tif demoInptEval.renderCnt != expectedRenderCnt {\n\t\tt.Errorf(\"%s: The number of render procedure invocations doesn't match expected value. It's called %d times, expected: %d \", caseDesc, demoInptEval.renderCnt, expectedRenderCnt)\n\t}\n\n\tif demoInptEval.aggrCnt != expectedAggrRenderCnt {\n\t\tt.Errorf(\"%s: The number of aggregation procedure invocations doesn't match expected value. It's called %d times, expected: %d \", caseDesc, demoInptEval.aggrCnt, expectedAggrRenderCnt)\n\t}\n}\n"
  },
  {
    "path": "msgservice/applicationscopeowner_test.go",
    "content": "package msgservice\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n)\n\nvar (\n\tscnWithOwners = `{\n\t\t\"image\":\"Demo mock image1\",\n\t\t\"registry\":\"registry1\",\n\t\t\"vulnerability_summary\":{\"critical\":0,\"high\":1,\"medium\":3,\"low\":4,\"negligible\":5},\n\t\t\"image_assurance_results\":{\"disallowed\":true},\n\t\t\"application_scope_owners\": [\"recipient1@aquasec.com\", \"recipient1@aquasec.com\"]\n\t}`\n)\n\nfunc TestApplicationScopeOwner(t *testing.T) {\n\tdbPathReal := dbservice.DbPath\n\tdefer func() {\n\t\tos.Remove(dbservice.DbPath)\n\t\tdbservice.ChangeDbPath(dbPathReal)\n\t}()\n\tdbservice.ChangeDbPath(\"test_webhooks.db\")\n\n\tdemoEmailAction := &DemoEmailAction{\n\t\temailCounts: 0,\n\t}\n\n\tsrvUrl := \"\"\n\tdemoRoute := &routes.InputRoute{}\n\n\tdemoRoute.Name = \"demo-route\"\n\n\tdemoInptEval := &DemoInptEval{}\n\n\tdemoEmailAction.wg = &sync.WaitGroup{}\n\tdemoEmailAction.wg.Add(1)\n\n\tsrv := new(MsgService)\n\tif srv.EvaluateRegoRule(demoRoute, []byte(scnWithOwners)) {\n\t\tsrv.MsgHandling([]byte(scnWithOwners), demoEmailAction, demoRoute, demoInptEval, &srvUrl)\n\t}\n\n\tdemoEmailAction.wg.Wait()\n\n\tif len(demoEmailAction.payloads) != 1 {\n\t\tt.Errorf(\"Action Send method isn't called as expected! Number of invocation expected %d, got: %d\", 1, len(demoEmailAction.payloads))\n\t}\n\tsent := demoEmailAction.payloads[0]\n\n\townersStr, ok := sent[\"owners\"]\n\tif !ok {\n\t\tt.Errorf(\"Owners key is missed from output payload\")\n\t}\n\towners := strings.Split(ownersStr, \";\")\n\tfor _, own := range owners {\n\t\tif own != \"recipient1@aquasec.com\" && own != \"recipient2@aquasec.com\" {\n\t\t\tt.Errorf(\"Unexpected owner value: '%s'\", own)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "msgservice/calculateexpired_test.go",
    "content": "package msgservice\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCalculateExpired(t *testing.T) {\n\ttimeouts := []int{0, 1, 2, 100}\n\n\tfor _, timeout := range timeouts {\n\t\tr := calculateExpired(timeout)\n\n\t\tif timeout == 0 {\n\t\t\tassert.Nil(t, r)\n\t\t} else {\n\t\t\tn := time.Now()\n\t\t\tdiff := r.Sub(n)\n\t\t\tassert.GreaterOrEqual(t, float64(timeout), diff.Seconds())\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "msgservice/getuniqueid_test.go",
    "content": "package msgservice\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\tunique_scan1 = `{\n\t\"image\":\"Demo mock image1\",\n\t\"registry\":\"registry1\",\n\t\"digest\":\"abc\",\n\t\"vulnerability_summary\":{\"critical\":0,\"high\":1,\"medium\":3,\"low\":4,\"negligible\":5},\n\t\"image_assurance_results\":{\"disallowed\":true}\n}`\n\tunique_scan2 = `{\n\t\"image\":\"Demo mock image2\",\n\t\"registry\":\"registry2\",\n\t\"digest\":\"def\",\n\t\"vulnerability_summary\":{\"critical\":0,\"high\":1,\"medium\":3,\"low\":4,\"negligible\":5},\n\t\"image_assurance_results\":{\"disallowed\":true}\n}`\n\tnon_unique_payload = `{\n\t\t\"action\": \"some\",\n\t\t\"adjective\": \"nice\",\n\t\t\"category\" : \"\",\n\t\t\"date\": 123,\n\t\t\"id\": 8,\n\t\t\"result\": 200,\n\t\t\"source_ip\": \"192.168.0.1\",\n\t\t\"time\": 45,\n\t\t\"type\": \"one\", \n\t\t\"user\": \"admin\",\n\t\t\"version\": \"2.0.1\"\n\t\t\n}`\n)\n\nfunc TestScanUniqueId(t *testing.T) {\n\ttests := []struct {\n\t\tinputs             []string\n\t\tcaseDesc           string\n\t\tuniqueMessageProps []string\n\t\texpctdInvc         int\n\t}{\n\t\t{\n\t\t\tinputs:             []string{unique_scan1, unique_scan1},\n\t\t\tcaseDesc:           \"Same scan twice with unique message props specified\",\n\t\t\tuniqueMessageProps: []string{\"digest\", \"image\", \"registry\"},\n\t\t\texpctdInvc:         1,\n\t\t},\n\t\t{\n\t\t\tinputs:     []string{unique_scan1, unique_scan1},\n\t\t\tcaseDesc:   \"Same scan twice without unique message props specified\",\n\t\t\texpctdInvc: 2,\n\t\t},\n\t\t{\n\t\t\tinputs:             []string{unique_scan1, unique_scan2},\n\t\t\tcaseDesc:           \"2 unique scan with unique message props specified\",\n\t\t\tuniqueMessageProps: []string{\"digest\", \"image\", \"registry\"},\n\t\t\texpctdInvc:         2,\n\t\t},\n\t\t{\n\t\t\tinputs:             []string{unique_scan1, unique_scan2},\n\t\t\tcaseDesc:           \"2 unique scan without unique message props specified\",\n\t\t\tuniqueMessageProps: []string{\"digest\", \"image\", \"registry\"},\n\t\t\texpctdInvc:         2,\n\t\t},\n\t\t{\n\t\t\tinputs:     []string{non_unique_payload, non_unique_payload},\n\t\t\tcaseDesc:   \"2 non-scan inputs without unique message props specified\",\n\t\t\texpctdInvc: 2,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tsendInputs(t, test.caseDesc, test.inputs, test.uniqueMessageProps, test.expctdInvc)\n\t}\n\n}\n\nfunc sendInputs(t *testing.T, caseDesc string, inputs []string, uniqueMessageProps []string, expected int) {\n\tdbPathReal := dbservice.DbPath\n\tdefer func() {\n\t\tos.Remove(dbservice.DbPath)\n\t\tdbservice.ChangeDbPath(dbPathReal)\n\t}()\n\tdbservice.ChangeDbPath(\"test_webhooks.db\")\n\n\tdemoEmailAction := &DemoEmailAction{\n\t\temailCounts: 0,\n\t}\n\n\tsrvUrl := \"\"\n\tdemoRoute := &routes.InputRoute{}\n\n\tdemoRoute.Name = \"demo-route\"\n\tdemoRoute.Plugins.UniqueMessageProps = uniqueMessageProps\n\n\tdemoInptEval := &DemoInptEval{}\n\n\tdemoEmailAction.wg = &sync.WaitGroup{}\n\tdemoEmailAction.wg.Add(expected)\n\n\tfor _, inp := range inputs {\n\t\tsrv := new(MsgService)\n\t\tif srv.EvaluateRegoRule(demoRoute, []byte(inp)) {\n\t\t\tsrv.MsgHandling([]byte(inp), demoEmailAction, demoRoute, demoInptEval, &srvUrl)\n\t\t}\n\t}\n\n\tdemoEmailAction.wg.Wait()\n\n\tif demoEmailAction.getEmailsCount() != expected {\n\t\tt.Errorf(\"[%s] Wrong number of Send method calls: expected %d, got %d\", caseDesc, expected, demoEmailAction.getEmailsCount())\n\t}\n\n}\n\nfunc TestGetMessageUniqueId(t *testing.T) {\n\ttests := []struct {\n\t\tprops    []string\n\t\tname     string\n\t\tcontext  map[string]interface{}\n\t\tfilename string\n\t\twantKey  string\n\t\twantErr  string\n\t}{\n\t\t{\n\t\t\tprops:   []string{\"name\"},\n\t\t\tname:    \"Single property\",\n\t\t\tcontext: map[string]interface{}{\"name\": \"alpine\"},\n\t\t\twantKey: \"alpine\",\n\t\t},\n\t\t{\n\t\t\tprops:   []string{\"name\", \"registry\"},\n\t\t\tname:    \"Multi property\",\n\t\t\tcontext: map[string]interface{}{\"name\": \"alpine\", \"registry\": \"registry2\"},\n\t\t\twantKey: \"alpine-registry2\",\n\t\t},\n\t\t{\n\t\t\tprops:   []string{\"name\", \"cnt\"},\n\t\t\tname:    \"Numeric\",\n\t\t\tcontext: map[string]interface{}{\"name\": \"alpine\", \"cnt\": 0},\n\t\t\twantKey: \"alpine-0\",\n\t\t},\n\t\t{\n\t\t\tprops:   []string{\"name\", \"registry\"},\n\t\t\tname:    \"Missed property\",\n\t\t\tcontext: map[string]interface{}{\"name\": \"alpine\"},\n\t\t\twantKey: \"alpine\",\n\t\t},\n\t\t{\n\t\t\tprops:   []string{\"name\", \"meta.category\"},\n\t\t\tname:    \"Multi Level Property\",\n\t\t\tcontext: map[string]interface{}{\"name\": \"alpine\", \"meta\": map[string]interface{}{\"category\": \"design\"}},\n\t\t\twantKey: \"alpine-design\",\n\t\t},\n\t\t{\n\t\t\tprops:   []string{\"name\", \"items.id\"},\n\t\t\tname:    \"Multi Level Property With Collection\",\n\t\t\tcontext: map[string]interface{}{\"name\": \"alpine\", \"items\": []interface{}{map[string]interface{}{\"id\": \"KLM\"}, map[string]interface{}{\"id\": \"DEF\"}}},\n\t\t\twantKey: \"alpine-KLM\",\n\t\t},\n\t\t{\n\t\t\tprops:   []string{\"name\", \"items.id\"},\n\t\t\tname:    \"Multi Level Property With Empty Collection\",\n\t\t\tcontext: map[string]interface{}{\"name\": \"alpine\", \"items\": []map[string]interface{}{}},\n\t\t\twantKey: \"alpine\",\n\t\t},\n\t\t{\n\t\t\tprops:   []string{\"name.id\"},\n\t\t\tname:    \"Multi Level Property Referencing String\",\n\t\t\tcontext: map[string]interface{}{\"name\": \"alpine\"},\n\t\t},\n\t\t{\n\t\t\tprops:    []string{\"digest\", \"image\", \"registry\", \"vulnerability_summary.critical\", \"vulnerability_summary.high\", \"vulnerability_summary.medium\", \"vulnerability_summary.low\"},\n\t\t\tname:     \"Legacy scan logic from Postee 1.0\",\n\t\t\tfilename: \"all-in-one-image.json\",\n\t\t\twantKey:  \"sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf-all-in-one:3.5.19223-Aqua-0-7-30-6\",\n\t\t},\n        {\n            props:    []string{\"arr.foo\"},\n            name:     \"Multi Level Property With Collection Of Interfaces\",\n            filename: \"collection-of-interfaces.json\",\n            wantKey:  \"bar\",\n        },\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tvar msg map[string]interface{}\n\t\t\tif test.filename != \"\" {\n\t\t\t\tfname := filepath.Join(\"testdata\", test.filename)\n\t\t\t\tb, err := os.ReadFile(fname)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\terr = json.Unmarshal(b, &msg)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\tmsg = test.context\n\t\t\t}\n\t\t\tkey := GetMessageUniqueId(msg, test.props)\n\t\t\tassert.Equal(t, test.wantKey, key)\n\t\t})\n\t}\n\n}\n"
  },
  {
    "path": "msgservice/logs.go",
    "content": "package msgservice\n\nimport \"log\"\n\nfunc prnInputLogs(msg string, v ...interface{}) {\n\tmaxLen := 20\n\tfor idx, e := range v {\n\t\tb, ok := e.([]byte)\n\t\tif ok {\n\t\t\tif l := len(b); l > maxLen {\n\t\t\t\tv[idx] = string(b[:maxLen])\n\t\t\t}\n\t\t}\n\t}\n\tlog.Printf(msg, v...)\n}\n"
  },
  {
    "path": "msgservice/msghandling.go",
    "content": "package msgservice\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/regoservice\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n)\n\ntype MsgService struct {\n}\n\nfunc (scan *MsgService) MsgHandling(input []byte, output actions.Action, route *routes.InputRoute, inpteval data.Inpteval, AquaServer *string) {\n\tif output == nil {\n\t\treturn\n\t}\n\n\tin := map[string]interface{}{}\n\tif err := json.Unmarshal(input, &in); err != nil {\n\t\tprnInputLogs(\"json.Unmarshal error for %q: %v\", input, err)\n\t\treturn\n\t}\n\n\t//TODO move logic below somewhere close to Jira action implementation\n\towners := \"\"\n\tapplicationScopeOwnersObj, ok := in[\"application_scope_owners\"]\n\tif ok {\n\t\tapplicationScopeOwners := make([]string, 0)\n\n\t\tfor _, owner := range applicationScopeOwnersObj.([]interface{}) {\n\t\t\tapplicationScopeOwners = append(applicationScopeOwners, owner.(string))\n\t\t}\n\n\t\tif len(applicationScopeOwners) > 0 {\n\t\t\towners = strings.Join(applicationScopeOwners, \";\")\n\t\t}\n\t}\n\n\tif route.Plugins.UniqueMessageProps != nil && len(route.Plugins.UniqueMessageProps) > 0 {\n\t\tmsgKey := GetMessageUniqueId(in, route.Plugins.UniqueMessageProps)\n\t\texpired := calculateExpired(route.Plugins.UniqueMessageTimeoutSeconds)\n\n\t\twasStored, err := dbservice.MayBeStoreMessage(input, msgKey, expired)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error while storing input: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tif !wasStored {\n\t\t\tlog.Printf(\"The same message was received before: %s\", msgKey)\n\t\t\treturn\n\t\t}\n\n\t}\n\n\tposteeOpts := map[string]string{\n\t\t\"AquaServer\": *AquaServer,\n\t}\n\n\tin[\"postee\"] = posteeOpts\n\n\tcontent, err := inpteval.Eval(in, *AquaServer)\n\tif err != nil {\n\t\tlog.Printf(\"Error while evaluating input: %v\", err)\n\t\treturn\n\t}\n\n\tif owners != \"\" {\n\t\tcontent[\"owners\"] = owners\n\t}\n\n\tif route.Plugins.AggregateMessageNumber > 0 && inpteval.IsAggregationSupported() {\n\t\taggregated := AggregateScanAndGetQueue(route.Name, content, route.Plugins.AggregateMessageNumber, false)\n\t\tif len(aggregated) > 0 {\n\t\t\tcontent, err = inpteval.BuildAggregatedContent(aggregated)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error while building aggregated content: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif route.SerializeActions {\n\t\t\t\tsend(output, content)\n\t\t\t} else {\n\t\t\t\tgo send(output, content)\n\t\t\t}\n\t\t}\n\t} else if route.Plugins.AggregateTimeoutSeconds > 0 && inpteval.IsAggregationSupported() {\n\t\tAggregateScanAndGetQueue(route.Name, content, 0, true)\n\n\t\tif !route.IsSchedulerRun() { //TODO route shouldn't have any associated logic\n\t\t\tlog.Printf(\"about to schedule %s\\n\", route.Name)\n\t\t\tRunScheduler(route, send, AggregateScanAndGetQueue, inpteval, &route.Name, output)\n\t\t} else {\n\t\t\tlog.Printf(\"%s is already scheduled\\n\", route.Name)\n\t\t}\n\t} else {\n\t\tif route.SerializeActions {\n\t\t\tsend(output, content)\n\t\t} else {\n\t\t\tgo send(output, content)\n\t\t}\n\t}\n}\n\n// EvaluateRegoRule returns true in case the given input ([]byte) matches the input of the given route\nfunc (scan *MsgService) EvaluateRegoRule(r *routes.InputRoute, input []byte) bool {\n\tin := map[string]interface{}{}\n\tif err := json.Unmarshal(input, &in); err != nil {\n\t\tprnInputLogs(\"json.Unmarshal error for %q: %v\", input, err)\n\t\treturn false\n\t}\n\n\t// input-files will override input, if both are provided\n\tif ok, err := regoservice.DoesMatchRegoCriteria(in, r.InputFiles, r.Input); err != nil {\n\t\tif !regoservice.IsUsedRegoFiles(r.InputFiles) {\n\t\t\tprnInputLogs(\"Error while evaluating rego rule %s :%v for the input %s\", r.Input, err, input)\n\t\t} else {\n\t\t\tprnInputLogs(\"Error while evaluating rego rule for input files :%v for the input %s\", err, input)\n\t\t}\n\t\treturn false\n\t} else if !ok {\n\t\tif !regoservice.IsUsedRegoFiles(r.InputFiles) {\n\t\t\tprnInputLogs(\"Input %s... doesn't match a REGO rule: %s\", input, r.Input)\n\t\t} else {\n\t\t\tprnInputLogs(\"Input %s... doesn't match a REGO input files rule\", input)\n\t\t}\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc send(otpt actions.Action, cnt map[string]string) {\n\terr := otpt.Send(cnt)\n\tif err != nil {\n\t\tlog.Printf(\"Error while sending event: %v\", err)\n\t}\n\n\terr = dbservice.RegisterPlgnInvctn(otpt.GetName())\n\tif err != nil {\n\t\tlog.Printf(\"Error while building aggregated content: %v\", err)\n\t\treturn\n\t}\n\n}\nfunc calculateExpired(UniqueMessageTimeoutSeconds int) *time.Time {\n\tif UniqueMessageTimeoutSeconds == 0 {\n\t\treturn nil\n\t}\n\ttimeToExpire := time.Duration(UniqueMessageTimeoutSeconds) * time.Second\n\texpired := time.Now().UTC().Add(timeToExpire)\n\treturn &expired\n}\n\nvar AggregateScanAndGetQueue = func(outputName string, currentContent map[string]string, counts int, ignoreLength bool) []map[string]string {\n\taggregatedScans, err := dbservice.AggregateScans(outputName, currentContent, counts, ignoreLength)\n\tif err != nil {\n\t\tlog.Printf(\"AggregateScans Error: %v\", err)\n\t\treturn aggregatedScans\n\t}\n\tif len(currentContent) != 0 && len(aggregatedScans) == 0 {\n\t\tlog.Printf(\"New scan was added to the queue of %q without sending.\", outputName)\n\t\treturn nil\n\t}\n\treturn aggregatedScans\n}\n"
  },
  {
    "path": "msgservice/msgservice_mocks_test.go",
    "content": "package msgservice\n\nimport (\n\t\"log\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nvar (\n\tmockScan1 = `{\"image\":\"Demo mock image1\",\"registry\":\"registry1\",\"vulnerability_summary\":{\"critical\":0,\"high\":1,\"medium\":3,\"low\":4,\"negligible\":5},\"image_assurance_results\":{\"disallowed\":true}}`\n\tmockScan2 = `{\"image\":\"Demo mock Image2\",\"registry\":\"registry2\",\"vulnerability_summary\":{\"critical\":0,\"high\":0,\"medium\":3,\"low\":4,\"negligible\":5},\"image_assurance_results\":{\"disallowed\":false}}`\n\tmockScan3 = `{\"image\":\"Demo mock Image3\",\"registry\":\"Registry3\",\"vulnerability_summary\":{\"critical\":0,\"high\":0,\"medium\":0,\"low\":4,\"negligible\":5},\"image_assurance_results\":{\"disallowed\":true}}`\n\tmockScan4 = `{\"image\":\"Demo mock image4\",\"registry\":\"registry4\",\"vulnerability_summary\":{\"critical\":0,\"high\":0,\"medium\":0,\"low\":0,\"negligible\":5},\"image_assurance_results\":{\"disallowed\":true}}`\n)\n\ntype DemoInptEval struct {\n\trndMu         sync.Mutex\n\taggrMu        sync.Mutex\n\trenderCnt     int\n\taggrCnt       int\n\tskipAggrSpprt bool\n}\n\nfunc (inptEval *DemoInptEval) Eval(in map[string]interface{}, serverUrl string) (map[string]string, error) {\n\tinptEval.rndMu.Lock()\n\tinptEval.renderCnt++\n\tinptEval.rndMu.Unlock()\n\ttitle := \"non-image\"\n\n\tif img, ok := in[\"image\"]; ok {\n\t\ttitle = img.(string)\n\t}\n\n\treturn map[string]string{\n\t\t\"title\":       title,\n\t\t\"description\": title,\n\t}, nil\n}\nfunc (inptEval *DemoInptEval) BuildAggregatedContent(items []map[string]string) (map[string]string, error) {\n\tinptEval.aggrMu.Lock()\n\tinptEval.aggrCnt++\n\tinptEval.aggrMu.Unlock()\n\n\tagrTitle := []string{}\n\tagrDescription := []string{}\n\tfor _, item := range items {\n\t\tagrTitle = append(agrTitle, item[\"title\"])\n\t\tagrDescription = append(agrDescription, item[\"description\"])\n\t}\n\n\treturn map[string]string{\n\t\t\"title\":       strings.Join(agrTitle, \",\"),\n\t\t\"description\": strings.Join(agrDescription, \",\"),\n\t}, nil\n}\nfunc (inptEval *DemoInptEval) IsAggregationSupported() bool {\n\treturn !inptEval.skipAggrSpprt\n}\n\ntype DemoEmailAction struct {\n\twg          *sync.WaitGroup\n\tmu          sync.Mutex\n\tpayloads    []map[string]string\n\temailCounts int\n}\n\nfunc (plg *DemoEmailAction) GetName() string {\n\treturn \"demo\"\n}\n\nfunc (plg *DemoEmailAction) getEmailsCount() int {\n\tplg.mu.Lock()\n\te := plg.emailCounts\n\tplg.mu.Unlock()\n\treturn e\n}\n\nfunc (plg *DemoEmailAction) Init() error { return nil }\nfunc (plg *DemoEmailAction) Send(data map[string]string) error {\n\tlog.Printf(\"Sending through demo plugin..\\n\")\n\tlog.Printf(\"%s\\n\", data[\"title\"])\n\n\tplg.mu.Lock()\n\tplg.emailCounts++\n\tplg.payloads = append(plg.payloads, data)\n\tplg.mu.Unlock()\n\tif plg.wg != nil {\n\t\tplg.wg.Done()\n\t}\n\treturn nil\n}\n\nfunc (plg *DemoEmailAction) Terminate() error { return nil }\nfunc (plg *DemoEmailAction) GetLayoutProvider() layout.LayoutProvider {\n\treturn new(formatting.HtmlProvider)\n}\n"
  },
  {
    "path": "msgservice/msgservice_scan_test.go",
    "content": "package msgservice\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nvar (\n\tAlpineImageResult = data.ScanImageInfo{\n\t\tImage:          \"alpine:3.8\",\n\t\tRegistry:       \"Docker Hub\",\n\t\tDigest:         \"sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1\",\n\t\tPreviousDigest: \"sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1\",\n\t\tImageAssuranceResults: data.ImageAssuranceResults{\n\t\t\tDisallowed: true,\n\t\t\tChecksPerformed: []data.ControlCheck{\n\t\t\t\t{Control: \"max_severity\", PolicyName: \"Default\", Failed: false},\n\t\t\t\t{Control: \"trusted_base_images\", PolicyName: \"Default\", Failed: true},\n\t\t\t\t{Control: \"max_score\", PolicyName: \"Default\", Failed: false},\n\t\t\t},\n\t\t},\n\t\tVulnerabilitySummary: data.VulnerabilitySummary{\n\t\t\tTotal: 2, Critical: 0, High: 0, Medium: 2, Low: 0, Negligible: 0, Sensitive: 0, Malware: 0,\n\t\t},\n\t\tScanOptions: data.ScanOptions{ScanSensitiveData: true, ScanMalware: true},\n\t\tResources: []data.InfoResources{\n\t\t\t{\n\t\t\t\tVulnerabilities: []data.Vulnerability{\n\t\t\t\t\t{Name: \"CVE-2018-20679\", Version: \"\", FixVersion: \"\", Severity: \"medium\"},\n\t\t\t\t\t{Name: \"CVE-2019-5747\", Version: \"\", FixVersion: \"\", Severity: \"medium\"},\n\t\t\t\t},\n\t\t\t\tResourceDetails: data.ResourceDetails{Name: \"busybox\", Version: \"1.28.4-r3\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tAshexPokemongoResult = data.ScanImageInfo{\n\t\tImage:          \"ashex/pokemongo-map:latest\",\n\t\tRegistry:       \"Docker Hub\",\n\t\tDigest:         \"sha256:ecc79e40b241b1b3b2580c58619cbc4c73b833308d780ad035bf6bdfbb529435\",\n\t\tPreviousDigest: \"sha256:ecc79e40b241b1b3b2580c58619cbc4c73b833308d780ad035bf6bdfbb529435\",\n\t\tImageAssuranceResults: data.ImageAssuranceResults{\n\t\t\tDisallowed: true, ChecksPerformed: []data.ControlCheck{\n\t\t\t\t{Control: \"trusted_base_images\", PolicyName: \"Default\", Failed: true},\n\t\t\t\t{Control: \"max_score\", PolicyName: \"Default\", Failed: true},\n\t\t\t\t{Control: \"max_severity\", PolicyName: \"Default\", Failed: true},\n\t\t\t},\n\t\t},\n\t\tVulnerabilitySummary: data.VulnerabilitySummary{\n\t\t\tTotal: 249, Critical: 2, High: 52, Medium: 184, Low: 11, Negligible: 34, Sensitive: 15, Malware: 0,\n\t\t},\n\t\tScanOptions: data.ScanOptions{ScanSensitiveData: true, ScanMalware: true},\n\t\tResources: []data.InfoResources{\n\t\t\t{\n\t\t\t\tVulnerabilities: []data.Vulnerability{\n\t\t\t\t\t{Name: \"WS-2018-0076\", Version: \"\", FixVersion: \"0.6.0\", Severity: \"negligible\"},\n\t\t\t\t\t{Name: \"\", Version: \"\", FixVersion: \"\", Severity: \"\"},\n\t\t\t\t},\n\t\t\t\tResourceDetails: data.ResourceDetails{Name: \"\", Version: \"\"},\n\t\t\t},\n\t\t},\n\t\tApplicationScopeOwners: []string{\"recipient1@aquasec.com\", \"recipient1@aquasec.com\"},\n\t}\n)\n\nfunc getImportantData(scan *data.ScanImageInfo) map[string]string {\n\timportant := make(map[string]string)\n\n\timportant[scan.Image] = \"scan.Image\"\n\timportant[scan.Registry] = \"\"\n\timportant[strconv.Itoa(scan.Critical)] = \"scan.Critical\"\n\timportant[strconv.Itoa(scan.High)] = \"scan.High\"\n\timportant[strconv.Itoa(scan.Medium)] = \"scan.Medium\"\n\timportant[strconv.Itoa(scan.Low)] = \"scan.Low\"\n\timportant[strconv.Itoa(scan.Negligible)] = \"scan.Negligible\"\n\n\tfor _, resource := range scan.Resources {\n\t\timportant[resource.Name] = \"resource.Name\"\n\t\timportant[resource.ResourceDetails.Name] = \"resource.ResourceDetails.Name\"\n\t\tfor _, vuln := range resource.Vulnerabilities {\n\t\t\timportant[vuln.Name] = \"vuln.Name\"\n\t\t\timportant[vuln.Version] = \"vuln.Version\"\n\t\t\timportant[vuln.FixVersion] = \"vuln.FixVersion\"\n\t\t}\n\t}\n\n\tfor i, check := range scan.ChecksPerformed {\n\t\tindex := strconv.Itoa(i + 1)\n\t\timportant[check.PolicyName] = index + \".check.PolicyName\"\n\t\timportant[check.Control] = index + \".check.Control\"\n\n\t\tpass := \"PASS\"\n\t\tif check.Failed {\n\t\t\tpass = \"FAIL\"\n\t\t}\n\t\timportant[pass] = pass\n\t}\n\treturn important\n}\n\nfunc Equal(A, B *data.ScanImageInfo) bool {\n\tif A.Image != B.Image || A.Registry != B.Registry ||\n\t\tA.ScanOptions != B.ScanOptions || A.VulnerabilitySummary != B.VulnerabilitySummary ||\n\t\tA.ImageAssuranceResults.Disallowed != B.ImageAssuranceResults.Disallowed ||\n\t\tlen(A.ImageAssuranceResults.ChecksPerformed) != len(B.ImageAssuranceResults.ChecksPerformed) {\n\t\treturn false\n\t}\n\n\tfor i, v := range A.ImageAssuranceResults.ChecksPerformed {\n\t\tif B.ImageAssuranceResults.ChecksPerformed[i] != v {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tfor i, v := range A.Resources {\n\t\tif len(v.Vulnerabilities) != len(B.Resources[i].Vulnerabilities) {\n\t\t\treturn false\n\t\t}\n\t\tfor j, vuln := range v.Vulnerabilities {\n\t\t\tif B.Resources[i].Vulnerabilities[j] != vuln {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\treturn true\n}\n\nfunc BenchmarkGenTicketDescription(b *testing.B) {\n\tprovider := new(formatting.JiraLayoutProvider)\n\tfor i := 0; i < b.N; i++ {\n\t\tlayout.GenTicketDescription(provider, &AlpineImageResult, nil, \"https://demolab.aquasec.com/\", \"\")\n\t}\n}\n\nfunc TestGenTicketDescription(t *testing.T) {\n\tvar tests = []struct {\n\t\tcurrentScan  *data.ScanImageInfo\n\t\tpreviousScan *data.ScanImageInfo\n\t}{\n\t\t{&AlpineImageResult, nil},\n\t\t{&AshexPokemongoResult, nil},\n\t}\n\n\tproviders := []layout.LayoutProvider{\n\t\tnew(formatting.JiraLayoutProvider),\n\t\tnew(formatting.HtmlProvider),\n\t}\n\n\tfor _, provider := range providers {\n\t\tfor _, test := range tests {\n\t\t\tgot := layout.GenTicketDescription(provider, test.currentScan, test.previousScan, \"https://demolab.aquasec.com\", \"\")\n\t\t\timportant := getImportantData(test.currentScan)\n\t\t\tfor k, v := range important {\n\t\t\t\tif !strings.Contains(got, k) {\n\t\t\t\t\tt.Errorf(\"Rendered data (%s) doesn't contain important value:\\n%s (%s)\\n\", got, k, v)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestGenTicketDescriptionFieldSeeMore(t *testing.T) {\n\tvar tests = []struct {\n\t\tname           string\n\t\tserverUrl      string\n\t\timage_url_part string\n\t\texpectedSuffix string\n\t}{\n\t\t{\"serverUrl is fill\", \"https://demolab.aquasec.com/\", \"alpine:3.9.6\",\n\t\t\t\"|CVE-2019-5747|busybox|1.28.4-r3|none|\\n\\nSee more: [https://demolab.aquasec.com/alpine:3.9.6|https://demolab.aquasec.com/alpine:3.9.6]\\n\"},\n\t\t{\"serverUrl is empty\", \"\", \"alpine:3.9.6\",\n\t\t\t\"|CVE-2019-5747|busybox|1.28.4-r3|none|\\n\\n\"},\n\t}\n\n\tprovider := new(formatting.JiraLayoutProvider)\n\tscan := &AlpineImageResult\n\n\tfor _, test := range tests {\n\t\tgot := layout.GenTicketDescription(provider, scan, nil, test.serverUrl, test.image_url_part)\n\t\tif !strings.HasSuffix(got, test.expectedSuffix) {\n\t\t\tt.Errorf(\"Rendered data doesn't have expected suffix:%s, got:%s\", test.expectedSuffix, got)\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "msgservice/msgservice_test.go",
    "content": "package msgservice\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n)\n\nvar (\n\tinvalidJson = `{\n\timage : \"My Image\"\n\t}`\n)\n\ntype FailingInptEval struct {\n\texpectedError     error\n\texpectedAggrError error\n}\n\nfunc (inptEval *FailingInptEval) Eval(in map[string]interface{}, serverUrl string) (map[string]string, error) {\n\tif inptEval.expectedError != nil {\n\t\treturn nil, inptEval.expectedError\n\t} else {\n\t\treturn map[string]string{\n\t\t\t\"title\":       \"some title\",\n\t\t\t\"description\": \"some description\",\n\t\t}, nil\n\t}\n}\nfunc (inptEval *FailingInptEval) BuildAggregatedContent(items []map[string]string) (map[string]string, error) {\n\n\treturn nil, inptEval.expectedAggrError\n}\nfunc (inptEval *FailingInptEval) IsAggregationSupported() bool {\n\treturn inptEval.expectedAggrError != nil\n}\n\nfunc TestInputs(t *testing.T) {\n\ttests := []struct {\n\t\tinput      []byte\n\t\tcaseDesc   string\n\t\tshouldPass bool\n\t}{\n\t\t{\n\t\t\tinput:      nil,\n\t\t\tcaseDesc:   \"Empty input\",\n\t\t\tshouldPass: false,\n\t\t},\n\t\t{\n\t\t\tinput:      []byte(invalidJson),\n\t\t\tcaseDesc:   \"Invalid Json\",\n\t\t\tshouldPass: false,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tvalidateInputValue(t, test.caseDesc, test.input, test.shouldPass)\n\t}\n\n}\nfunc validateInputValue(t *testing.T, caseDesc string, input []byte, shouldPass bool) {\n\tdbPathReal := dbservice.DbPath\n\tdefer func() {\n\t\tos.Remove(dbservice.DbPath)\n\t\tdbservice.ChangeDbPath(dbPathReal)\n\t}()\n\tdbservice.ChangeDbPath(\"test_webhooks.db\")\n\n\tdemoEmailAction := &DemoEmailAction{\n\t\temailCounts: 0,\n\t}\n\n\tsrvUrl := \"\"\n\texpected := 0\n\tif shouldPass {\n\t\texpected = 1\n\t}\n\n\tdemoRoute := &routes.InputRoute{}\n\n\tdemoRoute.Name = \"demo-route\"\n\n\tdemoInptEval := &DemoInptEval{}\n\n\tdemoEmailAction.wg = &sync.WaitGroup{}\n\tdemoEmailAction.wg.Add(expected)\n\n\tsrv := new(MsgService)\n\tif srv.EvaluateRegoRule(demoRoute, input) {\n\t\tsrv.MsgHandling(input, demoEmailAction, demoRoute, demoInptEval, &srvUrl)\n\t}\n\n\tdemoEmailAction.wg.Wait()\n\n\tif demoEmailAction.getEmailsCount() != expected {\n\t\tt.Errorf(\"[%s] Wrong number of Send method calls: expected %d, got %d\", caseDesc, expected, demoEmailAction.getEmailsCount())\n\t}\n\n}\nfunc TestEvalError(t *testing.T) {\n\tdbPathReal := dbservice.DbPath\n\tdefer func() {\n\t\tos.Remove(dbservice.DbPath)\n\t\tdbservice.ChangeDbPath(dbPathReal)\n\t}()\n\tdbservice.ChangeDbPath(\"test_webhooks.db\")\n\n\tdemoEmailAction := &DemoEmailAction{\n\t\temailCounts: 0,\n\t}\n\n\tsrvUrl := \"\"\n\n\tdemoRoute := &routes.InputRoute{}\n\n\tdemoRoute.Name = \"demo-route\"\n\tevalError := errors.New(\"eval error\")\n\n\tdemoInptEval := &FailingInptEval{\n\t\texpectedError: evalError,\n\t}\n\n\tsrv := new(MsgService)\n\tif srv.EvaluateRegoRule(demoRoute, []byte(mockScan1)) {\n\t\tsrv.MsgHandling([]byte(mockScan1), demoEmailAction, demoRoute, demoInptEval, &srvUrl)\n\t}\n\n\tif demoEmailAction.getEmailsCount() > 0 {\n\t\tt.Errorf(\"Action shouldn't be called when evaluation is failed\")\n\t}\n}\n\nfunc TestAggrEvalError(t *testing.T) {\n\tdbPathReal := dbservice.DbPath\n\tdefer func() {\n\t\tos.Remove(dbservice.DbPath)\n\t\tdbservice.ChangeDbPath(dbPathReal)\n\t}()\n\tdbservice.ChangeDbPath(\"test_webhooks.db\")\n\n\tdemoEmailAction := &DemoEmailAction{\n\t\temailCounts: 0,\n\t}\n\n\tsrvUrl := \"\"\n\n\tdemoRoute := &routes.InputRoute{}\n\n\tdemoRoute.Name = \"demo-route\"\n\tdemoRoute.Plugins.AggregateMessageNumber = 2\n\n\taggrEvalError := errors.New(\"aggregation eval error\")\n\n\tdemoInptEval := &FailingInptEval{\n\t\texpectedAggrError: aggrEvalError,\n\t}\n\n\tfor i := 0; i < 2; i++ {\n\t\tsrv := new(MsgService)\n\t\tif srv.EvaluateRegoRule(demoRoute, []byte(mockScan1)) {\n\t\t\tsrv.MsgHandling([]byte(mockScan1), demoEmailAction, demoRoute, demoInptEval, &srvUrl)\n\t\t}\n\t}\n\n\tif demoEmailAction.getEmailsCount() > 0 {\n\t\tt.Errorf(\"Action shouldn't be called when evaluation is failed\")\n\t}\n}\nfunc TestEmptyInput(t *testing.T) {\n\tdbPathReal := dbservice.DbPath\n\tdefer func() {\n\t\tos.Remove(dbservice.DbPath)\n\t\tdbservice.ChangeDbPath(dbPathReal)\n\t}()\n\tdbservice.ChangeDbPath(\"test_webhooks.db\")\n\n\tsrvUrl := \"\"\n\n\tdemoRoute := &routes.InputRoute{}\n\n\tdemoRoute.Name = \"demo-route\"\n\n\tdemoInptEval := &DemoInptEval{}\n\n\tsrv := new(MsgService)\n\tif srv.EvaluateRegoRule(demoRoute, []byte(\"{}\")) {\n\t\tsrv.MsgHandling([]byte(\"{}\"), nil, demoRoute, demoInptEval, &srvUrl)\n\t}\n\n\tif demoInptEval.renderCnt != 0 {\n\t\tt.Errorf(\"Eval() shouldn't be called if no output is passed to ResultHandling()\")\n\t}\n}\n\nfunc TestMalformedJSON(t *testing.T) {\n\tvar (\n\t\tsrvUrl          = \"\"\n\t\tdemoRoute       = &routes.InputRoute{Name: \"demo-route\"}\n\t\tdemoInptEval    = &DemoInptEval{}\n\t\tdemoEmailAction = &DemoEmailAction{}\n\t)\n\n\tsrv := new(MsgService)\n\tsrv.MsgHandling([]byte(\"{test:test}\"), demoEmailAction, demoRoute, demoInptEval, &srvUrl)\n\n\tif demoEmailAction.getEmailsCount() > 0 {\n\t\tt.Errorf(\"Action shouldn't be called when evaluation is failed\")\n\t}\n}\n"
  },
  {
    "path": "msgservice/regocriteria_test.go",
    "content": "package msgservice\n\nimport (\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n)\n\nvar (\n\tbadRego = `\n\tdefault input = false\n\t\n\thello {\n\t\tm := input.message\n\t\tm == \"world\"\n\t}\t\n`\n\tcorrectRego = `\n\tpackage postee\n\n\tdefault allow = false\n\t\n\tallow {\n\t\tcontains(input.image, \"image1\")\n\t}\n`\n)\n\nfunc TestRegoCriteria(t *testing.T) {\n\ttests := []struct {\n\t\tinput        string\n\t\tcaseDesc     string\n\t\tregoCriteria string\n\t\tregoFilePath string\n\t\tshouldPass   bool\n\t}{\n\t\t{\n\t\t\tinput:        mockScan1,\n\t\t\tcaseDesc:     \"Empty rule and files should allow\",\n\t\t\tregoCriteria: \"\",\n\t\t\tregoFilePath: \"\",\n\t\t\tshouldPass:   true,\n\t\t},\n\t\t{\n\t\t\tinput:        mockScan1,\n\t\t\tcaseDesc:     \"Matching rule\",\n\t\t\tregoCriteria: `contains(input.image, \"image1\")`,\n\t\t\tregoFilePath: \"\",\n\t\t\tshouldPass:   true,\n\t\t},\n\t\t{\n\t\t\tinput:        mockScan2,\n\t\t\tcaseDesc:     \"Not matching rule\",\n\t\t\tregoCriteria: `contains(input.image, \"image1\")`,\n\t\t\tregoFilePath: \"\",\n\t\t\tshouldPass:   false,\n\t\t},\n\t\t{\n\t\t\tinput:        mockScan1,\n\t\t\tcaseDesc:     \"Invalid rule\",\n\t\t\tregoCriteria: badRego,\n\t\t\tregoFilePath: \"\",\n\t\t\tshouldPass:   false,\n\t\t},\n\t\t{\n\t\t\tinput:        mockScan1,\n\t\t\tcaseDesc:     \"Matching file rule\",\n\t\t\tregoCriteria: correctRego,\n\t\t\tregoFilePath: \"../regoFile.rego\",\n\t\t\tshouldPass:   true,\n\t\t},\n\t\t{\n\t\t\tinput:        mockScan2,\n\t\t\tcaseDesc:     \"Not matching file rule\",\n\t\t\tregoCriteria: correctRego,\n\t\t\tregoFilePath: \"../regoFile.rego\",\n\t\t\tshouldPass:   false,\n\t\t},\n\t\t{\n\t\t\tinput:        mockScan1,\n\t\t\tcaseDesc:     \"Invalid file rule\",\n\t\t\tregoCriteria: badRego,\n\t\t\tregoFilePath: \"../regoFile.rego\",\n\t\t\tshouldPass:   false,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tvalidateRegoInput(t, test.caseDesc, test.input, test.regoCriteria, test.regoFilePath, test.shouldPass)\n\t}\n\n}\nfunc validateRegoInput(t *testing.T, caseDesc string, input string, regoCriteria string, regoFilePath string, shouldPass bool) {\n\tregoFile, err := os.Create(\"regoFile.rego\")\n\tif err != nil {\n\t\tt.Error(\"Can't create regoFile.rego file\")\n\t}\n\t_, err = regoFile.WriteString(regoCriteria)\n\tif err != nil {\n\t\tt.Error(\"Can't create regoFile.rego file\")\n\t}\n\tdefer os.Remove(\"regoFile.rego\")\n\tdefer regoFile.Close()\n\n\tdbPathReal := dbservice.DbPath\n\tdefer func() {\n\t\tos.Remove(dbservice.DbPath)\n\t\tdbservice.ChangeDbPath(dbPathReal)\n\t}()\n\tdbservice.ChangeDbPath(\"test_webhooks.db\")\n\n\tdemoEmailAction := &DemoEmailAction{\n\t\temailCounts: 0,\n\t}\n\n\tsrvUrl := \"\"\n\texpected := 0\n\tif shouldPass {\n\t\texpected = 1\n\t}\n\n\tdemoRoute := &routes.InputRoute{}\n\n\tdemoRoute.Name = \"demo-route\"\n\tdemoRoute.Input = regoCriteria\n\tdemoRoute.InputFiles = []string{regoFilePath}\n\n\tdemoInptEval := &DemoInptEval{}\n\n\tdemoEmailAction.wg = &sync.WaitGroup{}\n\tdemoEmailAction.wg.Add(expected)\n\n\tsrv := new(MsgService)\n\tif srv.EvaluateRegoRule(demoRoute, []byte(input)) {\n\t\tsrv.MsgHandling([]byte(input), demoEmailAction, demoRoute, demoInptEval, &srvUrl)\n\t}\n\n\tdemoEmailAction.wg.Wait()\n\n\tif demoEmailAction.getEmailsCount() != expected {\n\t\tt.Errorf(\"[%s] Wrong number of Send method calls: expected %d, got %d\", caseDesc, expected, demoEmailAction.getEmailsCount())\n\t}\n\n}\n"
  },
  {
    "path": "msgservice/scheduler.go",
    "content": "package msgservice\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n)\n\nvar getTicker = func(seconds int) *time.Ticker {\n\treturn time.NewTicker(time.Duration(seconds) * time.Second)\n}\nvar RunScheduler = func(\n\troute *routes.InputRoute,\n\tfnSend func(plg actions.Action, cnt map[string]string),\n\tfnAggregate func(outputName string, currentContent map[string]string, counts int, ignoreLength bool) []map[string]string,\n\tinpteval data.Inpteval,\n\tname *string,\n\toutput actions.Action,\n) {\n\tlog.Printf(\"Scheduler is activated for route %q. Period: %d sec\", route.Name, route.Plugins.AggregateTimeoutSeconds)\n\n\tticker := getTicker(route.Plugins.AggregateTimeoutSeconds)\n\troute.StartScheduler()\n\n\tgo func(done chan struct{}, currentTicker *time.Ticker) {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-done:\n\t\t\t\tcurrentTicker.Stop()\n\t\t\t\tlog.Printf(\"Scheduler for %q was stopped\", route.Name)\n\t\t\t\treturn\n\t\t\tcase <-currentTicker.C:\n\t\t\t\tlog.Printf(\"Scheduler triggered for %q\", route.Name)\n\t\t\t\tqueue := fnAggregate(route.Name, nil, 0, false)\n\t\t\t\tif len(queue) > 0 {\n\t\t\t\t\taggregated, err := inpteval.BuildAggregatedContent(queue)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tlog.Printf(\"Unable to build aggregated contents %v\\n\", err)\n\t\t\t\t\t}\n\t\t\t\t\tfnSend(output, aggregated)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}(route.Scheduling, ticker) //it has to be public to be used here.\n}\n"
  },
  {
    "path": "msgservice/scheduler_test.go",
    "content": "package msgservice\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestScheduler(t *testing.T) {\n\trouteName := \"test-schedule\"\n\tdemoRoute := &routes.InputRoute{}\n\tdemoRoute.Name = routeName\n\n\tdemoRoute.Plugins.AggregateTimeoutSeconds = 3\n\n\tdemoSend := func(plg actions.Action, cnt map[string]string) {\n\t\terr := plg.Send(cnt)\n\t\tif err != nil {\n\t\t\tt.Fatal(\"error Send\")\n\t\t}\n\t}\n\ttickerInvocations := 0\n\tdemoAggregate := func(outputName string, currentContent map[string]string, counts int, ignoreLength bool) []map[string]string {\n\t\ttickerInvocations++\n\t\treturn []map[string]string{\n\t\t\t{\n\t\t\t\t\"title\":       \"title1\",\n\t\t\t\t\"description\": \"description1\",\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"title\":       \"title2\",\n\t\t\t\t\"description\": \"description2\",\n\t\t\t},\n\t\t}\n\t}\n\tdemoInptEval := &DemoInptEval{}\n\n\tdemoEmailAction := &DemoEmailAction{\n\t\temailCounts: 0,\n\t}\n\n\tdemoEmailAction.wg = &sync.WaitGroup{}\n\tdemoEmailAction.wg.Add(1)\n\n\tRunScheduler(demoRoute, demoSend, demoAggregate, demoInptEval, &routeName, demoEmailAction)\n\n\tdemoEmailAction.wg.Wait()\n\tdemoRoute.StopScheduler()\n\n\ttime.Sleep(time.Duration(2*demoRoute.Plugins.AggregateTimeoutSeconds) * time.Second) //make sure ticker is not invoked anymore\n\n\tassert.Equal(t, 1, tickerInvocations)\n}\n"
  },
  {
    "path": "msgservice/testdata/all-in-one-image.json",
    "content": "{\n    \"image\": \"all-in-one:3.5.19223\",\n    \"registry\": \"Aqua\",\n    \"scan_started\": {\n        \"seconds\": 1624544066,\n        \"nanos\": 881635578\n    },\n    \"scan_duration\": 3,\n    \"pull_skipped\": true,\n    \"image_size\": 178041649,\n    \"digest\": \"sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf\",\n    \"os\": \"alpine\",\n    \"version\": \"3.8.4\",\n    \"resources\": [\n        {\n            \"resource\": {\n                \"type\": 2,\n                \"path\": \"/usr/local/bin/postgres\",\n                \"name\": \"postgresql\",\n                \"version\": \"9.5.14\",\n                \"cpe\": \"cpe:/a:postgresql:postgresql:9.5.14\",\n                \"layer_digest\": \"sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2018-1058\",\n                    \"description\": \"A flaw was found in the way Postgresql allowed a user to modify the behavior of a query for other users. An attacker with a user account could use this flaw to execute code with the permissions of superuser in the database. Versions 9.3 through 10 are affected.\",\n                    \"nvd_score\": 6.5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:S/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1058\",\n                    \"publish_date\": \"2018-03-02\",\n                    \"modification_date\": \"2019-10-09\",\n                    \"nvd_score_v3\": 8.8,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 6.5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:S/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 6.5\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 6.5\"\n                },\n                {\n                    \"name\": \"CVE-2018-1115\",\n                    \"description\": \"postgresql before versions 10.4, 9.6.9 is vulnerable in the adminpack extension, the pg_catalog.pg_logfile_rotate() function doesn't follow the same ACLs than pg_rorate_logfile. If the adminpack is added to a database, an attacker able to connect to it could exploit this to force log rotation.\",\n                    \"nvd_score\": 6.4,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:N/I:P/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1115\",\n                    \"publish_date\": \"2018-05-10\",\n                    \"modification_date\": \"2020-12-04\",\n                    \"fix_version\": \"9.6.9\",\n                    \"nvd_score_v3\": 9.1,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H\",\n                    \"nvd_severity_v3\": \"critical\",\n                    \"aqua_score\": 6.4,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:N/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 6.4\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 6.4\"\n                },\n                {\n                    \"name\": \"CVE-2018-16850\",\n                    \"description\": \"postgresql before versions 11.1, 10.6 is vulnerable to a to SQL injection in pg_upgrade and pg_dump via CREATE TRIGGER ... REFERENCING. Using a purpose-crafted trigger definition, an attacker can cause arbitrary SQL statements to run, with superuser privileges.\",\n                    \"nvd_score\": 7.5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"high\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-16850\",\n                    \"publish_date\": \"2018-11-13\",\n                    \"modification_date\": \"2019-10-09\",\n                    \"fix_version\": \"9.5.15\",\n                    \"nvd_score_v3\": 9.8,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"critical\",\n                    \"aqua_score\": 7.5,\n                    \"aqua_severity\": \"high\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 7.5\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 7.5\"\n                },\n                {\n                    \"name\": \"CVE-2019-10130\",\n                    \"description\": \"A vulnerability was found in PostgreSQL versions 11.x up to excluding 11.3, 10.x up to excluding 10.8, 9.6.x up to, excluding 9.6.13, 9.5.x up to, excluding 9.5.17. PostgreSQL maintains column statistics for tables. Certain statistics, such as histograms and lists of most common values, contain values taken from the column. PostgreSQL does not evaluate row security policies before consulting those statistics during query planning; an attacker can exploit this to read the most common values of certain columns. Affected columns are those for which the attacker has SELECT privilege and for which, in an ordinary query, row-level security prunes the set of rows visible to the attacker.\",\n                    \"nvd_score\": 4,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:S/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-10130\",\n                    \"publish_date\": \"2019-07-30\",\n                    \"modification_date\": \"2020-09-30\",\n                    \"fix_version\": \"9.5.17\",\n                    \"nvd_score_v3\": 4.3,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 4,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:S/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.0\"\n                },\n                {\n                    \"name\": \"CVE-2019-10208\",\n                    \"description\": \"A flaw was discovered in postgresql versions 9.4.x before 9.4.24, 9.5.x before 9.5.19, 9.6.x before 9.6.15, 10.x before 10.10 and 11.x before 11.5 where arbitrary SQL statements can be executed given a suitable SECURITY DEFINER function. An attacker, with EXECUTE permission on the function, can execute arbitrary SQL as the owner of the function.\",\n                    \"nvd_score\": 6.5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:S/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-10208\",\n                    \"publish_date\": \"2019-10-29\",\n                    \"modification_date\": \"2020-08-17\",\n                    \"fix_version\": \"9.5.19\",\n                    \"nvd_score_v3\": 8.8,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 6.5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:S/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 6.5\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 6.5\"\n                },\n                {\n                    \"name\": \"CVE-2020-10733\",\n                    \"description\": \"The Windows installer for PostgreSQL 9.5 - 12 invokes system-provided executables that do not have fully-qualified paths. Executables in the directory where the installer loads or the current working directory take precedence over the intended executables. An attacker having permission to add files into one of those directories can use this to execute arbitrary code with the installer's administrative rights.\",\n                    \"nvd_score\": 4.4,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:L/AC:M/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-10733\",\n                    \"publish_date\": \"2020-09-16\",\n                    \"modification_date\": \"2020-10-01\",\n                    \"fix_version\": \"9.5.22\",\n                    \"nvd_score_v3\": 7.3,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 4.4,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:L/AC:M/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.4\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.4\"\n                },\n                {\n                    \"name\": \"CVE-2020-14350\",\n                    \"description\": \"It was found that some PostgreSQL extensions did not use search_path safely in their installation script. An attacker with sufficient privileges could use this flaw to trick an administrator into executing a specially crafted script, during the installation or update of such extension. This affects PostgreSQL versions before 12.4, before 11.9, before 10.14, before 9.6.19, and before 9.5.23.\",\n                    \"nvd_score\": 4.4,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:L/AC:M/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-14350\",\n                    \"publish_date\": \"2020-08-24\",\n                    \"modification_date\": \"2020-09-18\",\n                    \"fix_version\": \"9.5.23\",\n                    \"nvd_score_v3\": 7.3,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 4.4,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:L/AC:M/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.4\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.4\"\n                },\n                {\n                    \"name\": \"CVE-2020-25694\",\n                    \"description\": \"A flaw was found in PostgreSQL versions before 13.1, before 12.5, before 11.10, before 10.15, before 9.6.20 and before 9.5.24. If a client application that creates additional database connections only reuses the basic connection parameters while dropping security-relevant parameters, an opportunity for a man-in-the-middle attack, or the ability to observe clear-text transmissions, could exist. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.\",\n                    \"nvd_score\": 6.8,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-25694\",\n                    \"publish_date\": \"2020-11-16\",\n                    \"modification_date\": \"2020-12-07\",\n                    \"fix_version\": \"9.5.24\",\n                    \"nvd_score_v3\": 8.1,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 6.8,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 6.8\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 6.8\"\n                },\n                {\n                    \"name\": \"CVE-2020-25695\",\n                    \"description\": \"A flaw was found in PostgreSQL versions before 13.1, before 12.5, before 11.10, before 10.15, before 9.6.20 and before 9.5.24. An attacker having permission to create non-temporary objects in at least one schema can execute arbitrary SQL functions under the identity of a superuser. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.\",\n                    \"nvd_score\": 6.5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:S/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-25695\",\n                    \"publish_date\": \"2020-11-16\",\n                    \"modification_date\": \"2020-12-07\",\n                    \"fix_version\": \"9.5.24\",\n                    \"nvd_score_v3\": 8.8,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 6.5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:S/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 6.5\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 6.5\"\n                },\n                {\n                    \"name\": \"CVE-2020-25696\",\n                    \"description\": \"A flaw was found in the psql interactive terminal of PostgreSQL in versions before 13.1, before 12.5, before 11.10, before 10.15, before 9.6.20 and before 9.5.24. If an interactive psql session uses \\\\gset when querying a compromised server, the attacker can execute arbitrary code as the operating system account running psql. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.\",\n                    \"nvd_score\": 7.6,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:H/Au:N/C:C/I:C/A:C\",\n                    \"nvd_severity\": \"high\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-25696\",\n                    \"publish_date\": \"2020-11-23\",\n                    \"modification_date\": \"2020-12-15\",\n                    \"fix_version\": \"9.5.24\",\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 7.6,\n                    \"aqua_severity\": \"high\",\n                    \"aqua_vectors\": \"AV:N/AC:H/Au:N/C:C/I:C/A:C\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 7.6\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 7.6\"\n                },\n                {\n                    \"name\": \"CVE-2021-3393\",\n                    \"description\": \"An information leak was discovered in postgresql in versions before 13.2, before 12.6 and before 11.11. A user having UPDATE permission but not SELECT permission to a particular column could craft queries which, under some circumstances, might disclose values from that column in error messages. An attacker could use this flaw to obtain information stored in a column they are allowed to write but not read.\",\n                    \"nvd_score\": 3.5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:S/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"low\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3393\",\n                    \"publish_date\": \"2021-04-01\",\n                    \"modification_date\": \"2021-06-04\",\n                    \"fix_version\": \"11.11\",\n                    \"nvd_score_v3\": 4.3,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 3.5,\n                    \"aqua_severity\": \"low\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:S/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 3.5\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 3.5\"\n                }\n            ]\n        },\n        {\n            \"resource\": {\n                \"format\": \"apk\",\n                \"name\": \"libgcrypt\",\n                \"version\": \"1.8.3-r0\",\n                \"arch\": \"x86_64\",\n                \"cpe\": \"pkg:/alpine:3.8.4:libgcrypt:1.8.3-r0\",\n                \"license\": \"LGPL\",\n                \"layer_digest\": \"sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785\",\n                \"src_name\": \"libgcrypt\",\n                \"src_version\": \"1.8.3-r0\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2019-12904\",\n                    \"description\": \"** DISPUTED ** In Libgcrypt 1.8.4, the C implementation of AES is vulnerable to a flush-and-reload side-channel attack because physical addresses are available to other processes. (The C implementation is used on platforms where an assembly-language implementation is unavailable.) NOTE: the vendor's position is that the issue report cannot be validated because there is no description of an attack.\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-12904\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-06-20\",\n                    \"modification_date\": \"2021-03-04\",\n                    \"fix_version\": \"1.8.3-r1\",\n                    \"solution\": \"Upgrade package libgcrypt to version 1.8.3-r1 or above.\",\n                    \"nvd_score_v3\": 5.9,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 328557,\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                },\n                {\n                    \"name\": \"CVE-2021-33560\",\n                    \"description\": \"Libgcrypt before 1.8.8 and 1.9.x before 1.9.3 mishandles ElGamal encryption because it lacks exponent blinding to address a side-channel attack against mpi_powm, and the window size is not chosen appropriately. (There is also an interoperability problem because the selection of the k integer value does not properly consider the differences between basic ElGamal encryption and generalized ElGamal encryption.) This, for example, affects use of ElGamal in OpenPGP.\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-33560\",\n                    \"publish_date\": \"2021-06-08\",\n                    \"modification_date\": \"2021-06-15\",\n                    \"fix_version\": \"1.8.8\",\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                }\n            ]\n        },\n        {\n            \"resource\": {\n                \"format\": \"apk\",\n                \"name\": \"musl-utils\",\n                \"version\": \"1.1.19-r10\",\n                \"arch\": \"x86_64\",\n                \"cpe\": \"pkg:/alpine:3.8.4:musl-utils:1.1.19-r10\",\n                \"license\": \"BSD,GPL2,MIT\",\n                \"layer_digest\": \"sha256:c87736221ed0bcaa60b8e92a19bec2284899ef89226f2a07968677cf59e637a4\",\n                \"src_name\": \"musl\",\n                \"src_version\": \"1.1.19-r10\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2019-14697\",\n                    \"description\": \"musl libc through 1.1.23 has an x87 floating-point stack adjustment imbalance, related to the math/i386/ directory. In some cases, use of this library could introduce out-of-bounds writes that are not present in an application's source code.\",\n                    \"nvd_score\": 7.5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"high\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-14697\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-08-06\",\n                    \"modification_date\": \"2020-03-14\",\n                    \"fix_version\": \"1.1.19-r11\",\n                    \"solution\": \"Upgrade package musl-utils to version 1.1.19-r11 or above.\",\n                    \"nvd_score_v3\": 9.8,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"critical\",\n                    \"aqua_score\": 7.5,\n                    \"aqua_severity\": \"high\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 328567,\n                    \"ancestor_pkg\": \"musl\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 7.5\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 7.5\"\n                }\n            ]\n        },\n        {\n            \"resource\": {\n                \"format\": \"apk\",\n                \"name\": \"busybox\",\n                \"version\": \"1.28.4-r3\",\n                \"arch\": \"x86_64\",\n                \"cpe\": \"pkg:/alpine:3.8.4:busybox:1.28.4-r3\",\n                \"license\": \"GPL2\",\n                \"layer_digest\": \"sha256:c87736221ed0bcaa60b8e92a19bec2284899ef89226f2a07968677cf59e637a4\",\n                \"src_name\": \"busybox\",\n                \"src_version\": \"1.28.4-r3\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2018-1000517\",\n                    \"description\": \"BusyBox project BusyBox wget version prior to commit 8e2174e9bd836e53c8b9c6e00d1bc6e2a718686e contains a Buffer Overflow vulnerability in Busybox wget that can result in heap buffer overflow. This attack appear to be exploitable via network connectivity. This vulnerability appears to have been fixed in after commit 8e2174e9bd836e53c8b9c6e00d1bc6e2a718686e.\",\n                    \"nvd_score\": 7.5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"high\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1000517\",\n                    \"publish_date\": \"2018-06-26\",\n                    \"modification_date\": \"2021-02-18\",\n                    \"fix_version\": \"1.29.0\",\n                    \"nvd_score_v3\": 9.8,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"critical\",\n                    \"aqua_score\": 7.5,\n                    \"aqua_severity\": \"high\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 7.5\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 7.5\"\n                },\n                {\n                    \"name\": \"CVE-2018-20679\",\n                    \"description\": \"An issue was discovered in BusyBox before 1.30.0. An out of bounds read in udhcp components (consumed by the DHCP server, client, and relay) allows a remote attacker to leak sensitive information from the stack by sending a crafted DHCP message. This is related to verification in udhcp_get_option() in networking/udhcp/common.c that 4-byte options are indeed 4 bytes.\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-20679\",\n                    \"publish_date\": \"2019-01-09\",\n                    \"modification_date\": \"2019-09-04\",\n                    \"fix_version\": \"1.30.0\",\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                },\n                {\n                    \"name\": \"CVE-2019-5747\",\n                    \"description\": \"An issue was discovered in BusyBox through 1.30.0. An out of bounds read in udhcp components (consumed by the DHCP server, client, and/or relay) might allow a remote attacker to leak sensitive information from the stack by sending a crafted DHCP message. This is related to assurance of a 4-byte length when decoding DHCP_SUBNET. NOTE: this issue exists because of an incomplete fix for CVE-2018-20679.\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-5747\",\n                    \"publish_date\": \"2019-01-09\",\n                    \"modification_date\": \"2019-09-04\",\n                    \"already_acknowledged\": true,\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                }\n            ]\n        },\n        {\n            \"resource\": {\n                \"format\": \"apk\",\n                \"name\": \"musl\",\n                \"version\": \"1.1.19-r10\",\n                \"arch\": \"x86_64\",\n                \"cpe\": \"pkg:/alpine:3.8.4:musl:1.1.19-r10\",\n                \"license\": \"MIT\",\n                \"layer_digest\": \"sha256:c87736221ed0bcaa60b8e92a19bec2284899ef89226f2a07968677cf59e637a4\",\n                \"src_name\": \"musl\",\n                \"src_version\": \"1.1.19-r10\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2019-14697\",\n                    \"description\": \"musl libc through 1.1.23 has an x87 floating-point stack adjustment imbalance, related to the math/i386/ directory. In some cases, use of this library could introduce out-of-bounds writes that are not present in an application's source code.\",\n                    \"nvd_score\": 7.5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"high\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-14697\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-08-06\",\n                    \"modification_date\": \"2020-03-14\",\n                    \"fix_version\": \"1.1.19-r11\",\n                    \"solution\": \"Upgrade package musl to version 1.1.19-r11 or above.\",\n                    \"already_acknowledged\": true,\n                    \"nvd_score_v3\": 9.8,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"critical\",\n                    \"aqua_score\": 7.5,\n                    \"aqua_severity\": \"high\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 328567,\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 7.5\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 7.5\"\n                },\n                {\n                    \"name\": \"CVE-2020-28928\",\n                    \"description\": \"In musl libc through 1.2.1, wcsnrtombs mishandles particular combinations of destination buffer size and source character limit, as demonstrated by an invalid write access (buffer overflow).\",\n                    \"nvd_score\": 2.1,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:L/AC:L/Au:N/C:N/I:N/A:P\",\n                    \"nvd_severity\": \"low\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-28928\",\n                    \"publish_date\": \"2020-11-24\",\n                    \"modification_date\": \"2021-06-08\",\n                    \"nvd_score_v3\": 5.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 2.1,\n                    \"aqua_severity\": \"low\",\n                    \"aqua_vectors\": \"AV:L/AC:L/Au:N/C:N/I:N/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 2.1\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 2.1\"\n                }\n            ]\n        },\n        {\n            \"resource\": {\n                \"format\": \"apk\",\n                \"name\": \"bash\",\n                \"version\": \"4.4.19-r1\",\n                \"arch\": \"x86_64\",\n                \"cpe\": \"pkg:/alpine:3.8.4:bash:4.4.19-r1\",\n                \"license\": \"GPL3orlater\",\n                \"layer_digest\": \"sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785\",\n                \"src_name\": \"bash\",\n                \"src_version\": \"4.4.19-r1\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2019-18276\",\n                    \"description\": \"An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \\\"saved UID\\\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \\\"enable -f\\\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.\",\n                    \"nvd_score\": 7.2,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:L/AC:L/Au:N/C:C/I:C/A:C\",\n                    \"nvd_severity\": \"high\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-18276\",\n                    \"publish_date\": \"2019-11-28\",\n                    \"modification_date\": \"2021-05-26\",\n                    \"nvd_score_v3\": 7.8,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 7.2,\n                    \"aqua_severity\": \"high\",\n                    \"aqua_vectors\": \"AV:L/AC:L/Au:N/C:C/I:C/A:C\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 7.2\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 7.2\"\n                }\n            ]\n        },\n        {\n            \"resource\": {\n                \"format\": \"apk\",\n                \"name\": \"libcrypto1.0\",\n                \"version\": \"1.0.2r-r0\",\n                \"arch\": \"x86_64\",\n                \"cpe\": \"pkg:/alpine:3.8.4:libcrypto1.0:1.0.2r-r0\",\n                \"license\": \"openssl\",\n                \"layer_digest\": \"sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785\",\n                \"src_name\": \"openssl\",\n                \"src_version\": \"1.0.2r-r0\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2019-1547\",\n                    \"description\": \"Normally in OpenSSL EC groups always have a co-factor present and this is used in side channel resistant code paths. However, in some cases, it is possible to construct a group using explicit parameters (instead of using a named curve). In those cases it is possible that such a group does not have the cofactor present. This can occur even where all the parameters match a known named curve. If such a curve is used then OpenSSL falls back to non-side channel resistant code paths which may result in full key recovery during an ECDSA signature operation. In order to be vulnerable an attacker would have to have the ability to time the creation of a large number of signatures where explicit parameters with no co-factor present are in use by an application using libcrypto. For the avoidance of doubt libssl is not vulnerable because explicit parameters are never used. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).\",\n                    \"nvd_score\": 1.9,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:L/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"low\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1547\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-09-10\",\n                    \"modification_date\": \"2020-10-20\",\n                    \"fix_version\": \"1.0.2t-r0\",\n                    \"solution\": \"Upgrade package libcrypto1.0 to version 1.0.2t-r0 or above.\",\n                    \"nvd_score_v3\": 4.7,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 1.9,\n                    \"aqua_severity\": \"low\",\n                    \"aqua_vectors\": \"AV:L/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 358269,\n                    \"ancestor_pkg\": \"openssl\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 1.9\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 1.9\"\n                },\n                {\n                    \"name\": \"CVE-2019-1563\",\n                    \"description\": \"In situations where an attacker receives automated notification of the success or failure of a decryption attempt an attacker, after sending a very large number of messages to be decrypted, can recover a CMS/PKCS7 transported encryption key or decrypt any RSA encrypted message that was encrypted with the public RSA key, using a Bleichenbacher padding oracle attack. Applications are not affected if they use a certificate together with the private RSA key to the CMS_decrypt or PKCS7_decrypt functions to select the correct recipient info to decrypt. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1563\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-09-10\",\n                    \"modification_date\": \"2020-10-20\",\n                    \"fix_version\": \"1.0.2t-r0\",\n                    \"solution\": \"Upgrade package libcrypto1.0 to version 1.0.2t-r0 or above.\",\n                    \"nvd_score_v3\": 3.7,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N\",\n                    \"nvd_severity_v3\": \"low\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 358270,\n                    \"ancestor_pkg\": \"openssl\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                },\n                {\n                    \"name\": \"CVE-2019-1551\",\n                    \"description\": \"There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1551\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-12-06\",\n                    \"modification_date\": \"2021-06-14\",\n                    \"fix_version\": \"1.0.2u-r0\",\n                    \"solution\": \"Upgrade package libcrypto1.0 to version 1.0.2u-r0 or above.\",\n                    \"nvd_score_v3\": 5.3,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 581142,\n                    \"ancestor_pkg\": \"openssl\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                },\n                {\n                    \"name\": \"CVE-2019-1552\",\n                    \"description\": \"OpenSSL has internal defaults for a directory tree where it can find a configuration file as well as certificates used for verification in TLS. This directory is most commonly referred to as OPENSSLDIR, and is configurable with the --prefix / --openssldir configuration options. For OpenSSL versions 1.1.0 and 1.1.1, the mingw configuration targets assume that resulting programs and libraries are installed in a Unix-like environment and the default prefix for program installation as well as for OPENSSLDIR should be '/usr/local'. However, mingw programs are Windows programs, and as such, find themselves looking at sub-directories of 'C:/usr/local', which may be world writable, which enables untrusted users to modify OpenSSL's default configuration, insert CA certificates, modify (or even replace) existing engine modules, etc. For OpenSSL 1.0.2, '/usr/local/ssl' is used as default for OPENSSLDIR on all Unix and Windows targets, including Visual C builds. However, some build instructions for the diverse Windows targets on 1.0.2 encourage you to specify your own --prefix. OpenSSL versions 1.1.1, 1.1.0 and 1.0.2 are affected by this issue. Due to the limited scope of affected deployments this has been assessed as low severity and therefore we are not creating new releases at this time. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).\",\n                    \"nvd_score\": 1.9,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:L/AC:M/Au:N/C:N/I:P/A:N\",\n                    \"nvd_severity\": \"low\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1552\",\n                    \"publish_date\": \"2019-07-30\",\n                    \"modification_date\": \"2020-12-23\",\n                    \"nvd_score_v3\": 3.3,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N\",\n                    \"nvd_severity_v3\": \"low\",\n                    \"aqua_score\": 1.9,\n                    \"aqua_severity\": \"low\",\n                    \"aqua_vectors\": \"AV:L/AC:M/Au:N/C:N/I:P/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 1.9\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 1.9\"\n                },\n                {\n                    \"name\": \"CVE-2020-1968\",\n                    \"description\": \"The Raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman (DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The attack can only be exploited if an implementation re-uses a DH secret across multiple TLS connections. Note that this issue only impacts DH ciphersuites and not ECDH ciphersuites. This issue affects OpenSSL 1.0.2 which is out of support and no longer receiving public updates. OpenSSL 1.1.1 is not vulnerable to this issue. Fixed in OpenSSL 1.0.2w (Affected 1.0.2-1.0.2v).\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-1968\",\n                    \"publish_date\": \"2020-09-09\",\n                    \"modification_date\": \"2021-06-14\",\n                    \"nvd_score_v3\": 3.7,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N\",\n                    \"nvd_severity_v3\": \"low\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                },\n                {\n                    \"name\": \"CVE-2020-1971\",\n                    \"description\": \"The X.509 GeneralName type is a generic type for representing different types of names. One of those name types is known as EDIPartyName. OpenSSL provides a function GENERAL_NAME_cmp which compares different instances of a GENERAL_NAME to see if they are equal or not. This function behaves incorrectly when both GENERAL_NAMEs contain an EDIPARTYNAME. A NULL pointer dereference and a crash may occur leading to a possible denial of service attack. OpenSSL itself uses the GENERAL_NAME_cmp function for two purposes: 1) Comparing CRL distribution point names between an available CRL and a CRL distribution point embedded in an X509 certificate 2) When verifying that a timestamp response token signer matches the timestamp authority name (exposed via the API functions TS_RESP_verify_response and TS_RESP_verify_token) If an attacker can control both items being compared then that attacker could trigger a crash. For example if the attacker can trick a client or server into checking a malicious certificate against a malicious CRL then this may occur. Note that some applications automatically download CRLs based on a URL embedded in a certificate. This checking happens prior to the signatures on the certificate and CRL being verified. OpenSSL's s_server, s_client and verify tools have support for the \\\"-crl_download\\\" option which implements automatic CRL downloading and this attack has been demonstrated to work against those tools. Note that an unrelated bug means that affected versions of OpenSSL cannot parse or construct correct encodings of EDIPARTYNAME. However it is possible to construct a malformed EDIPARTYNAME that OpenSSL's parser will accept and hence trigger this attack. All OpenSSL 1.1.1 and 1.0.2 versions are affected by this issue. Other OpenSSL releases are out of support and have not been checked. Fixed in OpenSSL 1.1.1i (Affected 1.1.1-1.1.1h). Fixed in OpenSSL 1.0.2x (Affected 1.0.2-1.0.2w).\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-1971\",\n                    \"publish_date\": \"2020-12-08\",\n                    \"modification_date\": \"2021-06-14\",\n                    \"nvd_score_v3\": 5.9,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                },\n                {\n                    \"name\": \"CVE-2021-23840\",\n                    \"description\": \"Calls to EVP_CipherUpdate, EVP_EncryptUpdate and EVP_DecryptUpdate may overflow the output length argument in some cases where the input length is close to the maximum permissible length for an integer on the platform. In such cases the return value from the function call will be 1 (indicating success), but the output length value will be negative. This could cause applications to behave incorrectly or crash. OpenSSL versions 1.1.1i and below are affected by this issue. Users of these versions should upgrade to OpenSSL 1.1.1j. OpenSSL versions 1.0.2x and below are affected by this issue. However OpenSSL 1.0.2 is out of support and no longer receiving public updates. Premium support customers of OpenSSL 1.0.2 should upgrade to 1.0.2y. Other users should upgrade to 1.1.1j. Fixed in OpenSSL 1.1.1j (Affected 1.1.1-1.1.1i). Fixed in OpenSSL 1.0.2y (Affected 1.0.2-1.0.2x).\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-23840\",\n                    \"publish_date\": \"2021-02-16\",\n                    \"modification_date\": \"2021-06-17\",\n                    \"fix_version\": \"1.0.2y\",\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                },\n                {\n                    \"name\": \"CVE-2021-23841\",\n                    \"description\": \"The OpenSSL public API function X509_issuer_and_serial_hash() attempts to create a unique hash value based on the issuer and serial number data contained within an X509 certificate. However it fails to correctly handle any errors that may occur while parsing the issuer field (which might occur if the issuer field is maliciously constructed). This may subsequently result in a NULL pointer deref and a crash leading to a potential denial of service attack. The function X509_issuer_and_serial_hash() is never directly called by OpenSSL itself so applications are only vulnerable if they use this function directly and they use it on certificates that may have been obtained from untrusted sources. OpenSSL versions 1.1.1i and below are affected by this issue. Users of these versions should upgrade to OpenSSL 1.1.1j. OpenSSL versions 1.0.2x and below are affected by this issue. However OpenSSL 1.0.2 is out of support and no longer receiving public updates. Premium support customers of OpenSSL 1.0.2 should upgrade to 1.0.2y. Other users should upgrade to 1.1.1j. Fixed in OpenSSL 1.1.1j (Affected 1.1.1-1.1.1i). Fixed in OpenSSL 1.0.2y (Affected 1.0.2-1.0.2x).\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-23841\",\n                    \"publish_date\": \"2021-02-16\",\n                    \"modification_date\": \"2021-06-17\",\n                    \"fix_version\": \"1.0.2y\",\n                    \"nvd_score_v3\": 5.9,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                }\n            ]\n        },\n        {\n            \"resource\": {\n                \"format\": \"apk\",\n                \"name\": \"libssl1.0\",\n                \"version\": \"1.0.2r-r0\",\n                \"arch\": \"x86_64\",\n                \"cpe\": \"pkg:/alpine:3.8.4:libssl1.0:1.0.2r-r0\",\n                \"license\": \"openssl\",\n                \"layer_digest\": \"sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785\",\n                \"src_name\": \"openssl\",\n                \"src_version\": \"1.0.2r-r0\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2019-1547\",\n                    \"description\": \"Normally in OpenSSL EC groups always have a co-factor present and this is used in side channel resistant code paths. However, in some cases, it is possible to construct a group using explicit parameters (instead of using a named curve). In those cases it is possible that such a group does not have the cofactor present. This can occur even where all the parameters match a known named curve. If such a curve is used then OpenSSL falls back to non-side channel resistant code paths which may result in full key recovery during an ECDSA signature operation. In order to be vulnerable an attacker would have to have the ability to time the creation of a large number of signatures where explicit parameters with no co-factor present are in use by an application using libcrypto. For the avoidance of doubt libssl is not vulnerable because explicit parameters are never used. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).\",\n                    \"nvd_score\": 1.9,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:L/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"low\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1547\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-09-10\",\n                    \"modification_date\": \"2020-10-20\",\n                    \"fix_version\": \"1.0.2t-r0\",\n                    \"solution\": \"Upgrade package libssl1.0 to version 1.0.2t-r0 or above.\",\n                    \"nvd_score_v3\": 4.7,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 1.9,\n                    \"aqua_severity\": \"low\",\n                    \"aqua_vectors\": \"AV:L/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 358269,\n                    \"ancestor_pkg\": \"openssl\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 1.9\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 1.9\"\n                },\n                {\n                    \"name\": \"CVE-2019-1563\",\n                    \"description\": \"In situations where an attacker receives automated notification of the success or failure of a decryption attempt an attacker, after sending a very large number of messages to be decrypted, can recover a CMS/PKCS7 transported encryption key or decrypt any RSA encrypted message that was encrypted with the public RSA key, using a Bleichenbacher padding oracle attack. Applications are not affected if they use a certificate together with the private RSA key to the CMS_decrypt or PKCS7_decrypt functions to select the correct recipient info to decrypt. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1563\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-09-10\",\n                    \"modification_date\": \"2020-10-20\",\n                    \"fix_version\": \"1.0.2t-r0\",\n                    \"solution\": \"Upgrade package libssl1.0 to version 1.0.2t-r0 or above.\",\n                    \"nvd_score_v3\": 3.7,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N\",\n                    \"nvd_severity_v3\": \"low\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 358270,\n                    \"ancestor_pkg\": \"openssl\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                },\n                {\n                    \"name\": \"CVE-2019-1551\",\n                    \"description\": \"There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1551\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-12-06\",\n                    \"modification_date\": \"2021-06-14\",\n                    \"fix_version\": \"1.0.2u-r0\",\n                    \"solution\": \"Upgrade package libssl1.0 to version 1.0.2u-r0 or above.\",\n                    \"nvd_score_v3\": 5.3,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 581142,\n                    \"ancestor_pkg\": \"openssl\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                },\n                {\n                    \"name\": \"CVE-2019-1552\",\n                    \"description\": \"OpenSSL has internal defaults for a directory tree where it can find a configuration file as well as certificates used for verification in TLS. This directory is most commonly referred to as OPENSSLDIR, and is configurable with the --prefix / --openssldir configuration options. For OpenSSL versions 1.1.0 and 1.1.1, the mingw configuration targets assume that resulting programs and libraries are installed in a Unix-like environment and the default prefix for program installation as well as for OPENSSLDIR should be '/usr/local'. However, mingw programs are Windows programs, and as such, find themselves looking at sub-directories of 'C:/usr/local', which may be world writable, which enables untrusted users to modify OpenSSL's default configuration, insert CA certificates, modify (or even replace) existing engine modules, etc. For OpenSSL 1.0.2, '/usr/local/ssl' is used as default for OPENSSLDIR on all Unix and Windows targets, including Visual C builds. However, some build instructions for the diverse Windows targets on 1.0.2 encourage you to specify your own --prefix. OpenSSL versions 1.1.1, 1.1.0 and 1.0.2 are affected by this issue. Due to the limited scope of affected deployments this has been assessed as low severity and therefore we are not creating new releases at this time. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).\",\n                    \"nvd_score\": 1.9,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:L/AC:M/Au:N/C:N/I:P/A:N\",\n                    \"nvd_severity\": \"low\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1552\",\n                    \"publish_date\": \"2019-07-30\",\n                    \"modification_date\": \"2020-12-23\",\n                    \"nvd_score_v3\": 3.3,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N\",\n                    \"nvd_severity_v3\": \"low\",\n                    \"aqua_score\": 1.9,\n                    \"aqua_severity\": \"low\",\n                    \"aqua_vectors\": \"AV:L/AC:M/Au:N/C:N/I:P/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 1.9\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 1.9\"\n                },\n                {\n                    \"name\": \"CVE-2020-1968\",\n                    \"description\": \"The Raccoon attack exploits a flaw in the TLS specification which can lead to an attacker being able to compute the pre-master secret in connections which have used a Diffie-Hellman (DH) based ciphersuite. In such a case this would result in the attacker being able to eavesdrop on all encrypted communications sent over that TLS connection. The attack can only be exploited if an implementation re-uses a DH secret across multiple TLS connections. Note that this issue only impacts DH ciphersuites and not ECDH ciphersuites. This issue affects OpenSSL 1.0.2 which is out of support and no longer receiving public updates. OpenSSL 1.1.1 is not vulnerable to this issue. Fixed in OpenSSL 1.0.2w (Affected 1.0.2-1.0.2v).\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-1968\",\n                    \"publish_date\": \"2020-09-09\",\n                    \"modification_date\": \"2021-06-14\",\n                    \"nvd_score_v3\": 3.7,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N\",\n                    \"nvd_severity_v3\": \"low\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                },\n                {\n                    \"name\": \"CVE-2020-1971\",\n                    \"description\": \"The X.509 GeneralName type is a generic type for representing different types of names. One of those name types is known as EDIPartyName. OpenSSL provides a function GENERAL_NAME_cmp which compares different instances of a GENERAL_NAME to see if they are equal or not. This function behaves incorrectly when both GENERAL_NAMEs contain an EDIPARTYNAME. A NULL pointer dereference and a crash may occur leading to a possible denial of service attack. OpenSSL itself uses the GENERAL_NAME_cmp function for two purposes: 1) Comparing CRL distribution point names between an available CRL and a CRL distribution point embedded in an X509 certificate 2) When verifying that a timestamp response token signer matches the timestamp authority name (exposed via the API functions TS_RESP_verify_response and TS_RESP_verify_token) If an attacker can control both items being compared then that attacker could trigger a crash. For example if the attacker can trick a client or server into checking a malicious certificate against a malicious CRL then this may occur. Note that some applications automatically download CRLs based on a URL embedded in a certificate. This checking happens prior to the signatures on the certificate and CRL being verified. OpenSSL's s_server, s_client and verify tools have support for the \\\"-crl_download\\\" option which implements automatic CRL downloading and this attack has been demonstrated to work against those tools. Note that an unrelated bug means that affected versions of OpenSSL cannot parse or construct correct encodings of EDIPARTYNAME. However it is possible to construct a malformed EDIPARTYNAME that OpenSSL's parser will accept and hence trigger this attack. All OpenSSL 1.1.1 and 1.0.2 versions are affected by this issue. Other OpenSSL releases are out of support and have not been checked. Fixed in OpenSSL 1.1.1i (Affected 1.1.1-1.1.1h). Fixed in OpenSSL 1.0.2x (Affected 1.0.2-1.0.2w).\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-1971\",\n                    \"publish_date\": \"2020-12-08\",\n                    \"modification_date\": \"2021-06-14\",\n                    \"nvd_score_v3\": 5.9,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                },\n                {\n                    \"name\": \"CVE-2021-23840\",\n                    \"description\": \"Calls to EVP_CipherUpdate, EVP_EncryptUpdate and EVP_DecryptUpdate may overflow the output length argument in some cases where the input length is close to the maximum permissible length for an integer on the platform. In such cases the return value from the function call will be 1 (indicating success), but the output length value will be negative. This could cause applications to behave incorrectly or crash. OpenSSL versions 1.1.1i and below are affected by this issue. Users of these versions should upgrade to OpenSSL 1.1.1j. OpenSSL versions 1.0.2x and below are affected by this issue. However OpenSSL 1.0.2 is out of support and no longer receiving public updates. Premium support customers of OpenSSL 1.0.2 should upgrade to 1.0.2y. Other users should upgrade to 1.1.1j. Fixed in OpenSSL 1.1.1j (Affected 1.1.1-1.1.1i). Fixed in OpenSSL 1.0.2y (Affected 1.0.2-1.0.2x).\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-23840\",\n                    \"publish_date\": \"2021-02-16\",\n                    \"modification_date\": \"2021-06-17\",\n                    \"fix_version\": \"1.0.2y\",\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                },\n                {\n                    \"name\": \"CVE-2021-23841\",\n                    \"description\": \"The OpenSSL public API function X509_issuer_and_serial_hash() attempts to create a unique hash value based on the issuer and serial number data contained within an X509 certificate. However it fails to correctly handle any errors that may occur while parsing the issuer field (which might occur if the issuer field is maliciously constructed). This may subsequently result in a NULL pointer deref and a crash leading to a potential denial of service attack. The function X509_issuer_and_serial_hash() is never directly called by OpenSSL itself so applications are only vulnerable if they use this function directly and they use it on certificates that may have been obtained from untrusted sources. OpenSSL versions 1.1.1i and below are affected by this issue. Users of these versions should upgrade to OpenSSL 1.1.1j. OpenSSL versions 1.0.2x and below are affected by this issue. However OpenSSL 1.0.2 is out of support and no longer receiving public updates. Premium support customers of OpenSSL 1.0.2 should upgrade to 1.0.2y. Other users should upgrade to 1.1.1j. Fixed in OpenSSL 1.1.1j (Affected 1.1.1-1.1.1i). Fixed in OpenSSL 1.0.2y (Affected 1.0.2-1.0.2x).\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-23841\",\n                    \"publish_date\": \"2021-02-16\",\n                    \"modification_date\": \"2021-06-17\",\n                    \"fix_version\": \"1.0.2y\",\n                    \"nvd_score_v3\": 5.9,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                }\n            ]\n        },\n        {\n            \"resource\": {\n                \"format\": \"apk\",\n                \"name\": \"libxml2\",\n                \"version\": \"2.9.8-r1\",\n                \"arch\": \"x86_64\",\n                \"cpe\": \"pkg:/alpine:3.8.4:libxml2:2.9.8-r1\",\n                \"license\": \"MIT\",\n                \"layer_digest\": \"sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785\",\n                \"src_name\": \"libxml2\",\n                \"src_version\": \"2.9.8-r1\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2019-19956\",\n                    \"description\": \"xmlParseBalancedChunkMemoryRecover in parser.c in libxml2 before 2.9.10 has a memory leak related to newDoc->oldNs.\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-19956\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-12-24\",\n                    \"modification_date\": \"2021-04-20\",\n                    \"fix_version\": \"2.9.8-r2\",\n                    \"solution\": \"Upgrade package libxml2 to version 2.9.8-r2 or above.\",\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:N/I:N/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 579780,\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                },\n                {\n                    \"name\": \"CVE-2021-3517\",\n                    \"description\": \"There is a flaw in the xml entity encoding functionality of libxml2 in versions before 2.9.11. An attacker who is able to supply a crafted file to be processed by an application linked with the affected functionality of libxml2 could trigger an out-of-bounds read. The most likely impact of this flaw is to application availability, with some potential impact to confidentiality and integrity if an attacker is able to use memory information to further exploit the application.\",\n                    \"nvd_score\": 7.5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"high\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3517\",\n                    \"publish_date\": \"2021-05-19\",\n                    \"modification_date\": \"2021-06-14\",\n                    \"fix_version\": \"2.9.11\",\n                    \"nvd_score_v3\": 8.6,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 7.5,\n                    \"aqua_severity\": \"high\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 7.5\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 7.5\"\n                },\n                {\n                    \"name\": \"CVE-2021-3518\",\n                    \"description\": \"There's a flaw in libxml2 in versions before 2.9.11. An attacker who is able to submit a crafted file to be processed by an application linked with libxml2 could trigger a use-after-free. The greatest impact from this flaw is to confidentiality, integrity, and availability.\",\n                    \"nvd_score\": 6.8,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3518\",\n                    \"publish_date\": \"2021-05-18\",\n                    \"modification_date\": \"2021-06-14\",\n                    \"fix_version\": \"2.9.11\",\n                    \"nvd_score_v3\": 8.8,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 6.8,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 6.8\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 6.8\"\n                },\n                {\n                    \"name\": \"CVE-2021-3537\",\n                    \"description\": \"A vulnerability found in libxml2 in versions before 2.9.11 shows that it did not propagate errors while parsing XML mixed content, causing a NULL dereference. If an untrusted XML document was parsed in recovery mode and post-validated, the flaw could be used to crash the application. The highest threat from this vulnerability is to system availability.\",\n                    \"nvd_score\": 4.3,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3537\",\n                    \"publish_date\": \"2021-05-14\",\n                    \"modification_date\": \"2021-06-14\",\n                    \"fix_version\": \"2.9.11\",\n                    \"nvd_score_v3\": 5.9,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n                    \"nvd_severity_v3\": \"medium\",\n                    \"aqua_score\": 4.3,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:N/I:N/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n                }\n            ]\n        },\n        {\n            \"resource\": {\n                \"format\": \"apk\",\n                \"name\": \"libxslt\",\n                \"version\": \"1.1.33-r1\",\n                \"arch\": \"x86_64\",\n                \"cpe\": \"pkg:/alpine:3.8.4:libxslt:1.1.33-r1\",\n                \"license\": \"custom\",\n                \"layer_digest\": \"sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785\",\n                \"src_name\": \"libxslt\",\n                \"src_version\": \"1.1.33-r1\"\n            },\n            \"scanned\": true,\n            \"vulnerabilities\": [\n                {\n                    \"name\": \"CVE-2019-13117\",\n                    \"description\": \"In numbers.c in libxslt 1.1.33, an xsl:number with certain format strings could lead to a uninitialized read in xsltNumberFormatInsertNumbers. This could allow an attacker to discern whether a byte on the stack contains the characters A, a, I, i, or 0, or any other character.\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-13117\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-07-01\",\n                    \"modification_date\": \"2020-08-24\",\n                    \"fix_version\": \"1.1.33-r3\",\n                    \"solution\": \"Upgrade package libxslt to version 1.1.33-r3 or above.\",\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 579832,\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                },\n                {\n                    \"name\": \"CVE-2019-13118\",\n                    \"description\": \"In numbers.c in libxslt 1.1.33, a type holding grouping characters of an xsl:number instruction was too narrow and an invalid character/length combination could be passed to xsltNumberFormatDecimal, leading to a read of uninitialized stack data.\",\n                    \"nvd_score\": 5,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-13118\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-07-01\",\n                    \"modification_date\": \"2020-08-24\",\n                    \"fix_version\": \"1.1.33-r3\",\n                    \"solution\": \"Upgrade package libxslt to version 1.1.33-r3 or above.\",\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 5,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 579836,\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n                },\n                {\n                    \"name\": \"CVE-2019-18197\",\n                    \"description\": \"In xsltCopyText in transform.c in libxslt 1.1.33, a pointer variable isn't reset under certain circumstances. If the relevant memory area happened to be freed and reused in a certain way, a bounds check could fail and memory outside a buffer could be written to, or uninitialized data could be disclosed.\",\n                    \"nvd_score\": 5.1,\n                    \"nvd_score_version\": \"CVSS v2\",\n                    \"nvd_vectors\": \"AV:N/AC:H/Au:N/C:P/I:P/A:P\",\n                    \"nvd_severity\": \"medium\",\n                    \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-18197\",\n                    \"vendor_score_version\": \"CVSS v2\",\n                    \"publish_date\": \"2019-10-18\",\n                    \"modification_date\": \"2020-08-24\",\n                    \"fix_version\": \"1.1.33-r2\",\n                    \"solution\": \"Upgrade package libxslt to version 1.1.33-r2 or above.\",\n                    \"nvd_score_v3\": 7.5,\n                    \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H\",\n                    \"nvd_severity_v3\": \"high\",\n                    \"aqua_score\": 5.1,\n                    \"aqua_severity\": \"medium\",\n                    \"aqua_vectors\": \"AV:N/AC:H/Au:N/C:P/I:P/A:P\",\n                    \"aqua_scoring_system\": \"CVSS V2\",\n                    \"heuristic_ref_id\": 358259,\n                    \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.1\",\n                    \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.1\"\n                }\n            ]\n        }\n    ],\n    \"image_assurance_results\": {\n        \"checks_performed\": [\n            {\n                \"policy_id\": 1,\n                \"policy_name\": \"Default\",\n                \"control\": \"malware\"\n            },\n            {\n                \"policy_id\": 1,\n                \"policy_name\": \"Default\",\n                \"control\": \"license\"\n            },\n            {\n                \"policy_id\": 1,\n                \"policy_name\": \"Default\",\n                \"control\": \"max_severity\",\n                \"maximum_severity_allowed\": \"critical\",\n                \"maximum_severity_found\": \"high\"\n            }\n        ]\n    },\n    \"vulnerability_summary\": {\n        \"total\": 43,\n        \"critical\": 0,\n        \"high\": 7,\n        \"medium\": 30,\n        \"low\": 6,\n        \"negligible\": 0,\n        \"sensitive\": 0,\n        \"malware\": 0,\n        \"score_average\": 5.020931\n    },\n    \"scan_options\": {\n        \"scan_executables\": true,\n        \"scan_sensitive_data\": true,\n        \"show_will_not_fix\": true,\n        \"webhook_url\": \"https://975cb1e5b1fc.ngrok.io\",\n        \"scan_malware\": true,\n        \"strict_scan\": true,\n        \"scan_files\": true,\n        \"scan_timeout\": 3600000000000,\n        \"manual_pull_fallback\": true,\n        \"dockerless\": true,\n        \"enable_fast_scanning\": true,\n        \"memoryThrottling\": true,\n        \"suggest_os_upgrade\": true,\n        \"seim_enabled\": true\n    },\n    \"previous_digest\": \"sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf\",\n    \"vulnerability_diff\": {\n        \"total\": 0,\n        \"critical\": 0,\n        \"high\": 0,\n        \"medium\": 0,\n        \"low\": 0,\n        \"negligible\": 0,\n        \"sensitive\": 0,\n        \"malware\": 0\n    },\n    \"initiating_user\": \"upwork\",\n    \"data_date\": 1624490283,\n    \"pull_name\": \"registry.aquasec.com/all-in-one:3.5.19223\",\n    \"changed_result\": false,\n    \"function_metadata\": {},\n    \"scan_id\": 386815,\n    \"required_image_platform\": \"amd64:::\",\n    \"scanned_image_platform\": \"amd64::linux:\",\n    \"security_feeds_used\": {\n        \"executables\": \"92475757e80429\"\n    },\n    \"image_id\": 704,\n    \"internal_digest_id\": {\n        \"id\": 1095\n    }\n}\n\n"
  },
  {
    "path": "msgservice/testdata/collection-of-interfaces.json",
    "content": "{\n  \"arr\": [\n    {\n      \"foo\": \"bar\"\n    },\n    {\n      \"foo\": \"bar2\"\n    }\n  ]\n}"
  },
  {
    "path": "msgservice/uniquemsgkey.go",
    "content": "package msgservice\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst (\n\tpropSep = \".\"\n)\n\nfunc GetMessageUniqueId(in map[string]interface{}, props []string) string {\n\tvalues := make([]string, 0)\n\tfor _, prop := range props {\n\t\tparts := strings.Split(prop, propSep)\n\t\tv := getSingleValue(in, parts)\n\t\tif v != \"\" {\n\t\t\tvalues = append(values, v)\n\t\t}\n\t}\n\treturn strings.Join(values, \"-\")\n}\n\nfunc getSingleValue(o interface{}, parts []string) string {\n\tin, ok := o.(map[string]interface{})\n\n\tif !ok {\n\t\treturn \"\"\n\t}\n\n\tif len(parts) == 1 {\n\t\tv, ok := in[parts[0]]\n\t\tif ok {\n\t\t\treturn fmt.Sprintf(\"%v\", v)\n\t\t}\n\t} else {\n\t\tpart := parts[0]\n\t\tv, ok := in[part]\n\t\tif ok {\n\t\t\tswitch x := v.(type) {\n\t\t\tcase map[string]interface{}:\n\t\t\t\treturn getSingleValue(x, parts[1:])\n\t\t\tcase []interface{}:\n\t\t\t\tif len(x) > 0 {\n\t\t\t\t\treturn getSingleValue(x[0], parts[1:]) //re-iterate with first element\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "overrides/main.html",
    "content": "{% extends \"base.html\" %}\n\n{% block outdated %}\nYou're not viewing the latest version.\n<a href=\"{{ '../' ~ base_url }}\">\n    <strong>Click here to go to latest.</strong>\n</a>\n{% endblock %}"
  },
  {
    "path": "rego-filters/Allow-Image-Name.rego",
    "content": "package postee\n\n\nArrayPermitedImageNames := {\"ubuntu\", \"busybox\"}#Comma separated list of images that will trigger the integration.\n\ndefault PermitImageNames = false\nPermitImageNames = true{ \n     contains(input.image, ArrayPermitedImageNames[_])\n}\n\nallow{\n   PermitImageNames\n}"
  },
  {
    "path": "rego-filters/Allow-Registry.rego",
    "content": "package postee\n\n\nArrayPermitedRegistry := {\"Aqua\"} #The list of registry name that triggers the integration.\n\ndefault PermitRegistry = false\nPermitRegistry = true{ \n     contains(input.registry, ArrayPermitedRegistry[_])\n}\n\nallow{\n   PermitRegistry\n}\n"
  },
  {
    "path": "rego-filters/Credential Access",
    "content": "package postee\n\n\nArrayBlockedSignaturesCredentialAccessCredentialAccess := {\n    \"TRC-8\", \"TRC-10\"\n}\n\ndefault BlockedSignaturesCredentialAccess = false\nBlockedSignaturesCredentialAccess = true{\n     contains(input.SigMetadata.ID, ArrayBlockedSignaturesCredentialAccessCredentialAccess[_])\n}\n\nallow{\n   BlockedSignaturesCredentialAccess\n}"
  },
  {
    "path": "rego-filters/Defense Evasion",
    "content": "package postee\n\n\nArrayBlockedSignaturesDefenseEvation := {\n    \"TRC-2\", \"TRC-3\", \"TRC-4\", \"TRC-9\", \"TRC-5\"\n}\n\ndefault BlockedSignaturesDefenseEvation = false\nBlockedSignaturesDefenseEvation = true{\n     contains(input.SigMetadata.ID, ArrayBlockedSignaturesDefenseEvation[_])\n}\n\nallow{\n   BlockedSignaturesDefenseEvation\n}"
  },
  {
    "path": "rego-filters/Ignore-Image-Name.rego",
    "content": "package postee\n\n\nArrayIgnoredImageNames := {\"alpine\", \"postgres\"} #List of comma separated images that will be ignored by the integration\n\ndefault IgnoreImageNames = true\nIgnoreImageNames = false{ \n     contains(input.image, ArrayIgnoredImageNames[_])\n}\n\nallow{\n   IgnoreImageNames\n}"
  },
  {
    "path": "rego-filters/Ignore-Registry.rego",
    "content": "package postee\n\n\nArrayIgnoreRegistry := {\"Aqua\"} #Comma separated list of registries that will be ignored by the integration\n\ndefault IgnoreRegistry = true\nIgnoreRegistry = false{ \n     contains(input.registry, ArrayIgnoreRegistry[_])\n}\n\nallow{\n   IgnoreRegistry\n}"
  },
  {
    "path": "rego-filters/Initial Access",
    "content": "package postee\n\n\nArrayBlockedSignaturesInitialAccess := {\n    \"TRC-12\"\n}\n\ndefault BlockedSignaturesInitialAccess = false\nBlockedSignaturesInitialAccess = true{\n     contains(input.SigMetadata.ID, ArrayBlockedSignaturesInitialAccess[_])\n}\n\nallow{\n   BlockedSignaturesInitialAccess\n}"
  },
  {
    "path": "rego-filters/Persistence",
    "content": "package postee\n\n\nArrayBlockedSignaturesPersistence := {\n    \"TRC-7\", \"TRC-15\"\n}\n\ndefault BlockedSignaturesPersistence = false\nBlockedSignaturesPersistence = true{\n     contains(input.SigMetadata.ID, ArrayBlockedSignaturesPersistence[_])\n}\n\nallow{\n   BlockedSignaturesPersistence\n}"
  },
  {
    "path": "rego-filters/Policy-Min-Vulnerability.rego",
    "content": "package postee\nimport future.keywords.in\n\n#Constants vulnerability values. Don't remove it!\nallVulnerability := {\"negligible\": 0, \"low\": 1, \"medium\": 2, \"high\": 3, \"critical\": 4}\n\nVulnerability := \"critical\"#The minimum vulnerability severity that triggers the integration.\n\ndefault PermitMinVulnerability = false\nPermitMinVulnerability = true{ \n     some i, val in allVulnerability\n\t\tval >= allVulnerability[Vulnerability]\n\t\tinput.vulnerability_summary[i] > 0\n}\n\nallow{\n\tPermitMinVulnerability\n}\n"
  },
  {
    "path": "rego-filters/Policy-Only-Fix-Available.rego",
    "content": "package postee\n\n#Trigger the integration only if image has a vulnerability with fix available (true). \n#If set to false, integration will be triggered even if all vulnerabilities has no fix available\ndefault PermitOnlyFixAvailable = false\nPermitOnlyFixAvailable = true{ \n     is_string(input.resources[_].vulnerabilities[_].fix_version)\n}\n\nallow{\n\tPermitOnlyFixAvailable\n}\n"
  },
  {
    "path": "rego-filters/Policy-Related-Features.rego",
    "content": "package postee\nimport future.keywords.in\n#Constants vulnerability values. Don't remove it!\nallVulnerability := {\"negligible\": 0, \"low\": 1, \"medium\": 2, \"high\": 3, \"critical\": 4}\n\nArrayPermitedImageNames := {\"ubuntu\", \"busybox\"} #Comma separated list of images that will trigger the integration.\nArrayIgnoredImageNames := {\"alpine\", \"postgres\"}  #List of comma separated images that will be ignored by the integration\nArrayPermitedRegistry := {\"Aqua\"} #The list of registry name that triggers the integration.\nArrayIgnoreRegistry := {\"Aqua\"} #Comma separated list of registries that will be ignored by the integration\nVulnerability := \"low\" #The minimum vulnerability severity that triggers the integration.\n\n\ndefault PermitImageNames = false\nPermitImageNames = true{ \n     contains(input.image, ArrayPermitedImageNames[_])\n}\n\ndefault IgnoreImageNames = true\nIgnoreImageNames = false{ \n     contains(input.image, ArrayIgnoredImageNames[_])\n}\n\ndefault PermitRegistry = false\nPermitRegistry = true{ \n     contains(input.registry, ArrayPermitedRegistry[_])\n}\n\ndefault IgnoreRegistry = true\nIgnoreRegistry = false{ \n     contains(input.registry, ArrayIgnoreRegistry[_])\n}\n\ndefault PermitMinVulnerability = false\nPermitMinVulnerability = true{ \n     some i, val in allVulnerability\n\t\tval >= allVulnerability[Vulnerability]\n\t\tinput.vulnerability_summary[i] > 0\n}\n\ndefault PermitOnlyFixAvailable = false\nPermitOnlyFixAvailable = true{ \n     is_string(input.resources[_].vulnerabilities[_].fix_version)\n}\n\n#Select the required functions. The functions will be conjunct as a logical \"AND\". \nallow{\n#     PermitImageNames\n#     IgnoreImageNames\n#     PermitRegistry\n#     IgnoreRegistry\n#     PermitMinVulnerability\n#     PermitOnlyFixAvailable\n}\n\n\n\n\n"
  },
  {
    "path": "rego-filters/Privilege Escalation",
    "content": "package postee\n\n\nArrayBlockedSignaturesPrivilegeEscalation := {\n    \"TRC-11\", \"TRC-14\"\n}\n\ndefault BlockedSignaturesPrivilegeEscalation = false\nBlockedSignaturesPrivilegeEscalation = true{\n     contains(input.SigMetadata.ID, ArrayBlockedSignaturesPrivilegeEscalation[_])\n}\n\nallow{\n   BlockedSignaturesPrivilegeEscalation\n}"
  },
  {
    "path": "rego-filters/Tracee Default Set",
    "content": "package postee\n\n\nArrayBlockedSignatures := {\n    \"TRC-1\", \"TRC-2\", \"TRC-3\", \"TRC-4\", \"TRC-5\",  \"TRC-6\", \"TRC-7\", \"TRC-8\", \"TRC-9\", \"TRC-10\", \"TRC-11\", \"TRC-12\", \"TRC-13\", \"TRC-14\"\n}\n\ndefault ArrayBlockedSignatures = false\nArrayBlockedSignatures = true{\n     contains(input.SigMetadata.ID, ArrayBlockedSignatures[_])\n}\n\nallow{\n   ArrayBlockedSignatures\n}\n"
  },
  {
    "path": "rego-filters/Trivy AWS Findings",
    "content": "package postee\n\nallow {\n    contains(input.Findings[0].ProductFields[\"Product Name\"], \"Trivy\")\n}"
  },
  {
    "path": "rego-templates/common/common.rego",
    "content": "package postee\n############################################# Common functions ############################################\nby_flag(a, b, flag) = a {\n\tflag\n}\nby_flag(a, b, flag) = b {\n\tflag = false\n}\nduplicate(a, b, col) = a {col == 1}\nduplicate(a, b, col) = b {col == 2}\n\nclamp(a, b) = b { a > b }\nclamp(a, b) = a { a <= b }\n\nflat_array(a) = o {\n\to:=[item |\n    \titem:=a[_][_]\n    ]\n}\nwith_default(obj, prop, default_value) = default_value{\n not obj[prop]\n}\nwith_default(obj, prop, default_value) = obj[prop]{\n obj[prop]\n}\n"
  },
  {
    "path": "rego-templates/example/audit-html.rego",
    "content": "package example.audit.html\n\n#Example of handling audit user\n\ntitle:=\"Audit event received\"\nresult:=sprintf(\"Audit event received from %s\", [input.user])"
  },
  {
    "path": "rego-templates/example/defectdojo/trivy-operator-defectdojo.rego",
    "content": "# METADATA\n# title: trivy-operator-defectdojo\n# scope: package\npackage plejd.trivyoperator.defectdojo\n\ntitle:=\"-\" #not used with webhook\n\n# allow environments or any other input being mapped\n# to DefectDojo specific meta data, input data can be consumed\n# from report labels applied by Trivy-operator\nmap_env_2_engagement := {\n\t\"development\": {\n    \"id\": 200,\n    \"name\": \"engagement-dev\"\n  },\n  \"production\": {\n    \"id\": 201,\n    \"name\": \"engagement-prod\"\n  },\n  \"sandbox\": {\n    \"id\": 133,\n    \"name\": \"engagement-plejdground\"\n  },\n  \"stage\": {\n    \"id\": 144,\n    \"name\": \"engagement-stage\"\n  },\n}\n\n# this following JSON structure's format is dictated by how the\n# underlying CURL command is expecting the incoming JSON payload\n# to look like. It uses mainly two components - `report` and\n# `metadata`.\ndd_data := {\n\t\"defectdojo\": {\n    \"scan\": input,\n    \"metadata\": {\n      \"active\": true,\n      \"engagement\": map_env_2_engagement[input.metadata.labels.env].id,\n      \"engagement_name\": map_env_2_engagement[input.metadata.labels.env].name,\n      \"environment\": input.metadata.labels.env,\n      \"minimum_severity\": \"Medium\",\n      \"product\": \"cluster\",\n      \"scan_date\": input.metadata.creationTimestamp,\n      \"scan_type\": \"Trivy Operator Scan\",\n      \"test_title\": \"\",\n      \"verified\": true \n    }\n  }\n}\n\n# METADATA\n# entrypoint: true\n# description: |\n#  Mangle a trivyoperator report and prepare it for being sent to DefectDojo.\n#  Note, that everything under the key defectdojo.metadata will be added as\n#  own FORM into the HTTP request sent to DefectDojo.\n# related_resources:\n# - ref: https://defectdojo.dev.plejd.io/api/v2/oa3/swagger-ui/\n#   description: \"Plejd DefectDojo instance, swagger API docs\"\n# organizations:\n# - Plejd AB\n# authors:\n# - name: Plejd CloudOps\n#   email: team-cloudops@plejd.com\nresult:=dd_data\n"
  },
  {
    "path": "rego-templates/example/defectdojo/trivy-operator-defectdojo_test.rego",
    "content": "package plejd.trivyoperator.defectdojo.test\n\nimport data.plejd.trivyoperator.defectdojo.result\n\n\ntest_a_allowed {\n\n  input_data := {\n    \"kind\": \"ClusterRbacAssessmentReport\",\n    \"metadata\": {\n      \"labels\": {\n        \"env\": \"development\"\n      },\n      \"creationTimestamp\": \"1234567890\"\n    }\n  }\n  exp_data := {\n    \"defectdojo\": {\n      \"scan\": input_data,\n      \"metadata\": {\n        \"active\": true,\n        \"engagement\": 200,\n        \"engagement_name\": \"engagement-dev\",\n        \"environment\": input_data.metadata.labels.env,\n        \"minimum_severity\": \"Medium\",\n        \"product\": \"cluster\",\n        \"scan_date\": input_data.metadata.creationTimestamp,\n        \"scan_type\": \"Trivy Operator Scan\",\n        \"test_title\": \"\",\n        \"verified\": true\n      }\n    }\n  }\n  result == exp_data with input as input_data\n}\n\ntodo_test_false {\n  false\n}\n\n\n"
  },
  {
    "path": "rego-templates/raw-message-html.rego",
    "content": "package postee.rawmessage.html\n\n\ntitle:=\"Raw Message Received\"\n\n# Postee injects custom function jsonformat() to pretty print objects\nresult:=sprintf(\"<pre><code>%s</code></pre>\",[jsonformat(input)])"
  },
  {
    "path": "rego-templates/raw-message-json.rego",
    "content": "package postee.rawmessage.json\n\n\ntitle:=\"-\" #not used with webhook\n\nresult:=jsonformat(input)"
  },
  {
    "path": "rego-templates/servicenow-incident.rego",
    "content": "package postee.servicenow.incident\n\nimport future.keywords\nimport data.postee.by_flag\nimport data.postee.with_default\n\n################################################ Templates ################################################\nresult_tpl = `\n<p><b>Name:</b> %s</p>\n<p><b>Category:</b> %s</p>\n<p><b>Severity:</b> %s</p>\n<p><b>Data:</b> %s</p>\n\n<p><b>Resourse policy name:</b> %s</p>\n<p><b>Resourse policy application scopes:</b> %s</p>\n`\nsummary_tpl =`Category: %s\nSeverity: %s`\n\ntable_tpl:=`\n<TABLE border='1' style='width: 100%%; border-collapse: collapse;'>\n%s\n</TABLE>\n`\n\ncell_tpl:=`<TD style='padding: 5px;'>%s</TD>\n`\n\nheader_tpl:=`<TH style='padding: 5px;'>%s</TH>\n`\n\nrow_tpl:=`\n<TR>\n%s\n</TR>`\n\n###########################################################################################################\ntitle = input.name\n\naggregation_pkg := \"postee.vuls.html.aggregation\"\n\ndata_list(d) := list {\n\tdat := split(d, \",\\\"\")\n\tsome i\n    list := [r |\n                    without_slash := replace(dat[i], \"\\\"\", \"\")\n                    without_open_bkt := replace(without_slash, \"{\", \"\")\n                    without_close_bkt := replace(without_open_bkt, \"}\", \"\")\n                    s := split(without_close_bkt, \":\")\n                    value_with_colon := trim_left(without_close_bkt, sprintf(\"%s\", [s[0]]))\n                    s[0] != \"tracee_finding\"\n                    r := [s[0], trim_left(value_with_colon, \":\")]\n    ]\n}\n\nrender_table_headers(headers) = row {\n    count(headers) > 0\n    ths := [th |\n        header := headers[_]\n        th := sprintf(header_tpl, [header])\n    ]\n\n    row := sprintf(row_tpl, [concat(\"\", ths)])\n}\n\n\nrender_table_headers(headers) = \"\" { #if headers not specified return empty results\n    count(headers) == 0\n}\n\nrender_table(headers, content_array) = s {\n\trows := [tr |\n    \t\t\tcells:=content_array[_]\n    \t\t\ttds:= [td |\n                \tctext:=cells[_]\n                    td := to_cell(ctext)\n                ]\n                tr=sprintf(row_tpl, [concat(\"\", tds)])\n    \t\t]\n\n\ts:=sprintf(table_tpl, [concat(\"\", array.concat([render_table_headers(headers)],rows))])\n}\n\n## why I added it?\nto_cell(txt) = c {\n    c:= sprintf(cell_tpl, [txt])\n}\n\nfound_data := with_default(input,\"data\", \"\")\nfound_severity :=  \"unknown\" if{\n    with_default(input,\"severity_score\", \"\") == \"\"\n}else = format_int(input.severity_score, 10)\n\n############################################## result values #############################################\nresult := res{\n    res = sprintf(result_tpl,[\n        with_default(input,\"name\", \"name not found\"),\n        with_default(input,\"category\", \"category not found\"),\n        found_severity,\n        by_flag(\n               \t\"data not found\",\n                render_table([], data_list(found_data)),\n            \tfound_data == \"\"),\n        with_default(input,\"response_policy_name\", \"response policy name not found\"),\n        with_default(input,\"application_scope\", \"none\"),\n    ])\n}\n\nresult_date = input.time\nresult_category = \"Security incident\"\n\nresult_assigned_to := by_flag(input.application_scope_owners[0], \"\", count(input.application_scope_owners) == 1)\nresult_assigned_group := by_flag(input.application_scope[0], \"\", count(input.application_scope) == 1)\n\nresult_severity := input.severity_score\n\nresult_summary := summary{\n    summary = sprintf(summary_tpl,[\n        with_default(input,\"category\", \"category not found\"),\n        found_severity,\n    ])\n}"
  },
  {
    "path": "rego-templates/servicenow-insight.rego",
    "content": "package postee.servicenow.insight\n\nimport future.keywords\nimport future.keywords.if\nimport data.postee.by_flag\nimport data.postee.with_default\n\n################################################ Templates ################################################\n#main template to render message\nhtml_tpl:=`\n<!-- Insight Details -->\n<h2> <i>Insight Details:</i> </h2>\n<p><b>Insight ID:</b> %s</p>\n<p><b>Description:</b> %s</p>\n<p><b>Impact:</b> %s</p>\n<p><b>Severity:</b> %s</p>\n<p><b>Found Date:</b> %s</p>\n<p><b>Last Scan:</b> %s</p>\n<p><b>URL:</b> %s</p>\n<!-- TODO  -->\n<!-- Resourse Details -->\n<h2> <i>Resourse Details:</i> </h2>\n<p><b>Resourse ID:</b> %s</p>\n<p><b>Resourse Name:</b> %s</p>\n<p><b>ARN:</b> %s</p>\n<p><b>Extra Info:</b> %s</p>\n<!-- Evidence -->\n<h2> <i>Evidence:</i> </h2>\n<!-- Vulnerabilities -->\n%s\n<!-- Sensitive data -->\n%s\n<!-- Recommendation -->\n<h2> <i>Recommendation:</i> </h2>\n%s\n<p><b>Resourse policy name:</b> %s</p>\n<p><b>Resourse policy application scopes:</b> %s</p>\n`\n\nsummary_tpl =`Insight ID: %s\nDescription: %s\nImpact: %s\nSeverity: %s\nFound Date: %s\nLast Scan: %s\nURL: %s`\n\nvlnrb_tpl = `\n<p>Vulnerabilities</p>\n%s\n`\n\nsensitive_data_tpl = `\n<p>Sensitive data</p>\n%s\n`\n\n#Extra % is required in width:100%\ntable_tpl:=`\n<TABLE border='1' style='width: 100%%; border-collapse: collapse;'>\n%s\n</TABLE>\n`\n\ncell_tpl:=`<TD style='padding: 5px;'>%s</TD>\n`\n\nheader_tpl:=`<TH style='padding: 5px;'>%s</TH>\n`\n\nrow_tpl:=`\n<TR>\n%s\n</TR>`\n\n###########################################################################################################\n\n############################################## Html rendering #############################################\n\nrender_table_headers(headers) = row {\n    count(headers) > 0\n    ths := [th |\n        header := headers[_]\n        th := sprintf(header_tpl, [header])\n    ]\n\n    row := sprintf(row_tpl, [concat(\"\", ths)])\n}\n\n\nrender_table_headers(headers) = \"\" { #if headers not specified return empty results\n    count(headers) == 0\n}\n\n\nrender_table(headers, content_array) = s {\n\trows := [tr |\n    \t\t\tcells:=content_array[_]\n    \t\t\ttds:= [td |\n                \tctext:=cells[_]\n                    td := to_cell(ctext)\n                ]\n                tr=sprintf(row_tpl, [concat(\"\", tds)])\n    \t\t]\n\n\ts:=sprintf(table_tpl, [concat(\"\", array.concat([render_table_headers(headers)],rows))])\n}\n\n## why I added it?\nto_cell(txt) = c {\n    c:= sprintf(cell_tpl, [txt])\n}\n\n\n####################################### Template specific functions #######################################\n# TODO refactor to support different properties\ncheck_failed(item) = false {\nnot item.failed #Either absent or false\n}\ncheck_failed(item) = true {\n item.failed\n}\n\n################################### Vulnerability table ##############################################\nvlnrb_headers := [\"Vulnerability ID\", \"Severity\", \"Resource name\", \"Installed version\", \"Fix version\"]\n\n\nrender_vlnrb(list) = sprintf(vlnrb_tpl, [render_table(vlnrb_headers, list)]) {\n    count(list) > 0\n}\n\nrender_vlnrb(list) = \"\" {  #returns empty string if list of vulnerabilities is passed\n    count(list) == 0\n}\n\nvln_list = vlnrb {\n    some i\n\tvlnrb := [r |\n                    vlnname := input.evidence.vulnerabilities[i].name\n                    severity := input.evidence.vulnerabilities[i].severity\n                    fxvrsn := with_default(input.evidence.vulnerabilities[i],\"fix_version\", \"none\")\n                    package_name = with_default(input.evidence.vulnerabilities[i], \"package_name\", \"none\")\n                    package_version = with_default(input.evidence.vulnerabilities[i], \"current_version\", \"none\")\n\n                    r := [vlnname, severity, package_name, package_version, fxvrsn]\n              ]\n}\n\n################################### Sensitive data table ##############################################\nsensitive_data_headers := [\"File Type\", \"File Path\", \"Image\"]\n\n\nrender_sensitive_data(list) = sprintf(sensitive_data_tpl, [render_table(sensitive_data_headers, list)]) {\n    count(list) > 0\n}\n\nrender_sensitive_data(list) = \"\" {  #returns empty string if list of sensitive data is passed\n    count(list) == 0\n}\n\nsensitive_data_list = vlnrb {\n    some i\n\tvlnrb := [r |\n                    file_type := input.evidence.sensitive_data[i].file_type\n                    file_path := input.evidence.sensitive_data[i].file_path\n                    image := input.evidence.sensitive_data[i].image\n\n                    r := [file_type, file_path, image]\n              ]\n}\n\n###########################################################################################################\npostee := with_default(input, \"postee\", {})\naqua_server := with_default(postee, \"AquaServer\", \"\")\nserver_url := trim_suffix(aqua_server, \"/#/images/\")\n\ntitle = input.insight.description\nhref := sprintf(\"%s/ah/#/%s/%s/%s/%s\", [server_url, \"insights\", urlquery.encode(input.insight.id), \"resource\", urlquery.encode(input.resource.id)])\ntext :=  sprintf(\"%s/ah/#/%s/%s/%s/%s\", [server_url, \"insights\", input.insight.id, \"resource\", input.resource.id])\n\naggregation_pkg := \"postee.vuls.html.aggregation\"\n\npriority_as_text = \"critical\" if {\n    input.insight.priority == 4\n}else = \"high\" if {\n    input.insight.priority == 3\n}else = \"medium\" if {\n    input.insight.priority == 2\n}else = \"low\" if {\n    input.insight.priority == 1\n}else = \"negligible\" if {\n    input.insight.priority == 0\n}else = \"unknown\"\n\nremediation_with_default(default_value) = default_value{\n  input.evidence.vulnerabilities_remediation==null; input.evidence.sensitive_data_remediation==\"\"; input.evidence.malware_remediation==\"\"\n}\n\nremediation_with_default(default_value) = val{\n  val := input.evidence.vulnerabilities_remediation\n  input.evidence.vulnerabilities_remediation!=null; input.evidence.sensitive_data_remediation==\"\"; input.evidence.malware_remediation==\"\"\n}\n\nremediation_with_default(default_value) = val{\n  val := input.evidence.vulnerabilities_remediation\n  input.evidence.vulnerabilities_remediation!=null; input.evidence.sensitive_data_remediation!=\"\"; input.evidence.malware_remediation==\"\"\n}\n\nremediation_with_default(default_value) = val{\n  val := input.evidence.sensitive_data_remediation\n  val !=\"\";input.evidence.vulnerabilities_remediation==null; input.evidence.malware_remediation==\"\"\n}\n\nremediation_with_default(default_value) = val{\n  val := input.evidence.malware_remediation\n  val != \"\"; input.evidence.vulnerabilities_remediation==null; input.evidence.sensitive_data_remediation==\"\"\n}\n\n############################################## result values #############################################\nresult = msg {\n\n    msg := sprintf(html_tpl, [\n    input.insight.id,\n    input.insight.description,\n    input.insight.impact,\n    priority_as_text,\n    input.resource.found_date,\n    input.resource.last_scanned,\n    by_flag(\n        \"\",\n        sprintf(`<a href='%s'>%s</a>`,[href, text]), #link\n        server_url == \"\"),\n    input.resource.id,\n    input.resource.name,\n    input.resource.arn,\n    input.resource.steps,\n    render_vlnrb(vln_list),\n    render_sensitive_data(sensitive_data_list),\n    remediation_with_default(\"No Recommendation\"),\n    input.response_policy_name,\n    with_default(input,\"application_scope\", \"none\"),\n    ])\n}\n\nresult_category = \"Security insight\"\n\nresult_assigned_to := by_flag(input.application_scope_owners[0], \"\", count(input.application_scope_owners) == 1)\nresult_assigned_group := by_flag(input.application_scope[0], \"\", count(input.application_scope) == 1)\n\nresult_severity := input.insight.priority\n\nresult_summary := summary{\n    summary = sprintf(summary_tpl,[\n    input.insight.id,\n        input.insight.description,\n        input.insight.impact,\n        priority_as_text,\n        input.resource.found_date,\n        input.resource.last_scanned,\n        by_flag(\n            \"\",\n            text, #link\n            server_url == \"\"),\n    ])\n}"
  },
  {
    "path": "rego-templates/servicenow.rego",
    "content": "package postee.servicenow\n\nimport future.keywords\nimport future.keywords.if\nimport data.postee.by_flag\nimport data.postee.with_default\n\n################################################ Templates ################################################\n#main template to render message\nhtml_tpl:=`\n<p><b>Name:</b> %s</p>\n<p><b>Registry:</b> %s</p>\n<p><b>Malware found:</b> %s</p>\n<p><b>Sensitive data found:</b> %s</p>\n<!-- Stats -->\n<h3> Vulnerability summary </h3>\n%s\n<!-- Assurance controls -->\n%s\n<!-- Critical severity vulnerabilities -->\n%s\n<!-- High severity vulnerabilities -->\n%s\n<!-- Medium severity vulnerabilities -->\n%s\n<!-- Low severity vulnerabilities -->\n%s\n<!-- Negligible severity vulnerabilities -->\n%s\n<p><b>Resourse policy name:</b> %s</p>\n<p><b>Resourse policy application scopes:</b> %s</p>\n%s\n`\n\nsummary_tpl =`Name: %s\nRegistry: %s\n%s\n%s\n\nvulnerabilities:\n*   critical: %d,\n*   high: %d,\n*   medium: %d,\n*   low: %d,\n*   negligible: %d\n\n%s`\n\nvlnrb_tpl = `\n<h3>%s severity vulnerabilities</h3>\n%s\n`\n\nassurance_control_tpl = `\n<h3>Assurance controls</h3>\n%s\n`\n\n#Extra % is required in width:100%\ntable_tpl:=`\n<TABLE border='1' style='width: 100%%; border-collapse: collapse;'>\n%s\n</TABLE>\n`\n\ncell_tpl:=`<TD style='padding: 5px;'>%s</TD>\n`\n\nheader_tpl:=`<TH style='padding: 5px;'>%s</TH>\n`\n\nrow_tpl:=`\n<TR>\n%s\n</TR>`\n\ncolored_text_tpl:=\"<span style='color:%s'>%s</span>\"\n\n###########################################################################################################\n\n############################################## Html rendering #############################################\n\nrender_table_headers(headers) = row {\n    count(headers) > 0\n    ths := [th |\n        header := headers[_]\n        th := sprintf(header_tpl, [header])\n    ]\n\n    row := sprintf(row_tpl, [concat(\"\", ths)])\n}\n\n\nrender_table_headers(headers) = \"\" { #if headers not specified return empty results\n    count(headers) == 0\n}\n\n\nrender_table(headers, content_array) = s {\n\trows := [tr |\n    \t\t\tcells:=content_array[_]\n    \t\t\ttds:= [td |\n                \tctext:=cells[_]\n                    td := to_cell(ctext)\n                ]\n                tr=sprintf(row_tpl, [concat(\"\", tds)])\n    \t\t]\n\n\ts:=sprintf(table_tpl, [concat(\"\", array.concat([render_table_headers(headers)],rows))])\n}\n\n## why I added it?\nto_cell(txt) = c {\n    c:= sprintf(cell_tpl, [txt])\n}\n\nto_colored_text(color, txt) = spn {\n    spn :=sprintf(colored_text_tpl, [color, txt])\n}\n\n####################################### Template specific functions #######################################\nto_severity_color(color, level) = spn {\n spn:=to_colored_text(color, format_int(with_default(input.vulnerability_summary,level,0), 10))\n}\n# TODO refactor to support different properties\ncheck_failed(item) = false {\nnot item.failed #Either absent or false\n}\ncheck_failed(item) = true {\n item.failed\n}\n\n# 2 dimension array for vulnerabilities summary\nseverities_stats := [\n                        [\"critical\", to_severity_color(\"#c00000\", \"critical\")],\n                        [\"high\", to_severity_color(\"#e0443d\", \"high\")],\n                        [\"medium\", to_severity_color(\"#f79421\", \"medium\")],\n                        [\"low\", to_severity_color(\"#e1c930\", \"low\")],\n                        [\"negligible\", to_severity_color(\"green\", \"negligible\")]\n                    ]\n\n# 2 dimension array for assurance controls\nassurance_controls := [ control |\n                    item := input.image_assurance_results.checks_performed[i]\n                    control := [format_int(i+1, 10), item.control,item.policy_name,\n                                            by_flag(\n                                                \"FAIL\",\n                                                \"PASS\",\n                                                check_failed(item)\n                                            )\n                    ]\n]\n\nvlnrb_headers := [\"Vulnerability ID\", \"Resource name\", \"Installed version\", \"Fix version\"]\n\n\nrender_vlnrb(severity, list) = sprintf(vlnrb_tpl, [severity, render_table(vlnrb_headers, list)]) {\n    count(list) > 0\n}\n\nrender_vlnrb(severity, list) = \"\" {  #returns empty string if list of vulnerabilities is passed\n    count(list) == 0\n}\n\nassurance_control_headers := [\"#\",\"Control\",\"Policy Name\", \"Status\"]\n\nrender_assurance_control(list) = sprintf(assurance_control_tpl, [render_table(assurance_control_headers, list)]) {\n    count(list) > 0\n}\n\nrender_assurance_control(list) = \"\" {  #returns empty string if list of assurance control is passed\n    count(list) == 0\n}\n\n# builds 2-dimension array for vulnerability table\nvln_list(severity) = vlnrb {\n    some i, j\n\tvlnrb := [r |\n                    item := input.resources[i]\n\n\n                    resource := item.resource\n                    vlnname := item.vulnerabilities[j].name\n                    fxvrsn := with_default(item.vulnerabilities[j],\"fix_version\", \"none\")\n                    resource_name = with_default(resource, \"name\", \"none\")\n                    resource_version = with_default(resource, \"version\", \"none\")\n\n                    item.vulnerabilities[j].aqua_severity == severity # only items with severity matched\n                    r := [vlnname, resource_name, resource_version, fxvrsn]\n              ]\n}\n###########################################################################################################\npostee := with_default(input, \"postee\", {})\naqua_server := with_default(postee, \"AquaServer\", \"\")\nserver_url := trim_suffix(aqua_server, \"images/\")\n\nreport_type := \"function\" if{\n    input.entity_type == 1\n} else = \"vm\" if{\n    input.entity_type == 2\n} else = \"image\"\n\ntitle = sprintf(`Aqua security | %s | %s | Scan report`, [report_type, input.image])\n\n## url formats:\n## function: <server_url>/#/functions/<registry>/<image>\n## vm: <server_url>/#/infrastructure/<image>/node\n## image: <server_url>/#/image/<registry>/<image>\nhref := sprintf(\"%s%s/%s/%s\", [server_url, \"functions\", urlquery.encode(input.registry), urlquery.encode(input.image)])  if{\n    report_type == \"function\"\n} else = sprintf(\"%s%s/%s/%s\", [server_url, \"infrastructure\", urlquery.encode(input.image), \"node\"]){\n    report_type == \"vm\"\n} else = sprintf(\"%s%s/%s/%s\", [server_url, \"image\", urlquery.encode(input.registry), urlquery.encode(input.image)])\n\ntext :=  sprintf(\"%s%s/%s/%s\", [server_url, \"functions\", input.registry, input.image]) if{\n    report_type == \"function\"\n} else = sprintf(\"%s%s/%s/%s\", [server_url, \"infrastructure\", input.image, \"node\"]) {\n    report_type == \"vm\"\n} else = sprintf(\"%s%s/%s/%s\", [server_url, report_type, input.registry, input.image])\n\nurl := by_flag(\"\", href, server_url == \"\")\n\n# some vulnerability_summary fields may not exist\ndefault vulnerability_summary_critical := 0\nvulnerability_summary_critical := input.vulnerability_summary.critical\ndefault vulnerability_summary_high := 0\nvulnerability_summary_high := input.vulnerability_summary.high\ndefault vulnerability_summary_medium := 0\nvulnerability_summary_medium := input.vulnerability_summary.medium\ndefault vulnerability_summary_low := 0\nvulnerability_summary_low := input.vulnerability_summary.low\ndefault vulnerability_summary_negligible := 0\nvulnerability_summary_negligible := input.vulnerability_summary.negligible\n\naggregation_pkg := \"postee.vuls.html.aggregation\"\n\n############################################## result values #############################################\nresult = msg {\n\n    msg := sprintf(html_tpl, [\n    input.image,\n    input.registry,\n\tby_flag(\n     \"Yes\",\n     \"No\",\n     input.scan_options.scan_malware #reflects current logic\n    ),\n\tby_flag(\n\t \"Yes\",\n     \"No\",\n     input.scan_options.scan_sensitive_data #reflects current logic\n\t),\n    render_table([], severities_stats),\n    render_assurance_control(assurance_controls),\n    render_vlnrb(\"Critical\", vln_list(\"critical\")),\n    render_vlnrb(\"High\", vln_list(\"high\")),\n    render_vlnrb(\"Medium\", vln_list(\"medium\")),\n    render_vlnrb(\"Low\", vln_list(\"low\")),\n    render_vlnrb(\"Negligible\", vln_list(\"negligible\")),\n    with_default(input,\"response_policy_name\", \"\"),\n    with_default(input,\"application_scope\", \"none\"),\n    by_flag(\n     \"\",\n     sprintf(`<p><b>See more:</b> <a href='%s'>%s</a></p>`,[href, text]), #link\n     server_url == \"\")\n    ])\n}\n\nresult_date = input.scan_started.seconds\n\nresult_category = \"Serverless functions Scanning\" if {\n    report_type == \"function\"\n}else = \"Security - VM Scan results\" if {\n    report_type == \"vm\"\n}else = \"Security Image Scan results\"\n\nresult_subcategory = \"Security incident\"\nresult_assigned_to := by_flag(input.application_scope_owners[0], \"\", count(input.application_scope_owners) == 1)\nresult_assigned_group := by_flag(input.application_scope[0], \"\", count(input.application_scope) == 1)\n\nresult_severity := 1 if {\n    input.vulnerability_summary.critical > 0\n} else = 2 if {\n    input.vulnerability_summary.high > 0\n} else = 3\n\nresult_summary := summary{\n    summary = sprintf(summary_tpl,[\n    input.image,\n    input.registry,\n\tby_flag(\n     \"Malware found: Yes\",\n     \"Malware found: No\",\n     input.scan_options.scan_malware #reflects current logic\n    ),\n\tby_flag(\n\t \"Sensitive data found: Yes\",\n     \"Sensitive data found: No\",\n     input.scan_options.scan_sensitive_data #reflects current logic\n\t),\n\tvulnerability_summary_critical,\n\tvulnerability_summary_high,\n\tvulnerability_summary_medium,\n\tvulnerability_summary_low,\n\tvulnerability_summary_negligible,\n\tby_flag(\n         \"\",\n         sprintf(`See more: %s`,[text]), #link\n         server_url == \"\"),\n    ])\n}"
  },
  {
    "path": "rego-templates/tracee-html.rego",
    "content": "package postee.tracee.html\n\n#Example of handling tracee event\n\ntitle:=sprintf(\"Tracee Detection - %s\", [input.SigMetadata.Name])\n\ntpl :=`\n<p> Rule Description: %s </p>\n<p> Detection: %s </p>\n<p> MITRE Details: %s </p>\n<p> Severity: %v </p>\n`\n\nresult:= res {\n res:= sprintf(tpl, [\n input.SigMetadata.Description,\n input.Context.processName,\n input.SigMetadata.Properties,\n input.SigMetadata.Properties.Severity\n ])\n }"
  },
  {
    "path": "rego-templates/tracee-slack.rego",
    "content": "package postee.tracee.slack\n\n#Example of handling tracee event\n\ntitle:=sprintf(\"Tracee Detection - %s\", [input.SigMetadata.Name])\n\nresult:= res {\n res:= [\n \t{ \"type\":\"section\",\n\t  \"text\": {\"type\":\"mrkdwn\",\"text\": sprintf(\"*Rule Description:* %s\", [input.SigMetadata.Description])}},\n \t{ \"type\":\"section\",\n\t  \"text\": {\"type\":\"mrkdwn\",\"text\": sprintf(\"*Detection:* %s\", [input.Context.processName])}},\n \t{ \"type\":\"section\",\n\t  \"text\": {\"type\":\"mrkdwn\",\"text\": sprintf(\"*MITRE Details:* %v\", [input.SigMetadata.Properties])}},\n\t{ \"type\":\"section\",\n\t  \"text\": {\"type\":\"mrkdwn\",\"text\": sprintf(\"*Severity:* %v\", [input.SigMetadata.Properties.Severity])}}\n ]\n}"
  },
  {
    "path": "rego-templates/trivy-jira.rego",
    "content": "package postee.trivy.jira\n############################################# Common functions ############################################\nwith_default(obj, prop, default_value) = default_value {\n    not obj[prop]\n}\n\nwith_default(obj, prop, default_value) = obj[prop] {\n    obj[prop]\n}\n\n#import common.by_flag\n################################################ Templates ################################################\n#main template to render message\n\ntpl:=`\nh1. Image name: %s\n%s\n%s\n%s\n%s\n%s\n`\n\nvlnrb_tpl = `\nh4. %s severity vulnerabilities\n%s\n`\n#Extra % is required in width:100%\n\ntable_tpl := `\n%s\n`\n\ncell_tpl := `| %s `\n\nheader_tpl := `|| %s `\n\nrow_tpl := `\n| %s `\n\ncolored_text_tpl := \"{color:%s}%s{color}\"\n\n###########################################################################################################\n\n############################################## Html rendering #############################################\n\nrender_table_headers(headers) = row {\n    count(headers) > 0\n    ths := [th |\n        header := headers[_]\n        th := sprintf(header_tpl, [header])\n    ]\n\n    row := sprintf(row_tpl, [concat(\"\", ths)])\n}\n\nrender_table_headers(headers) = \"\" { #if headers not specified return empty results\n    count(headers) == 0\n}\n\nrender_table(headers, content_array) = s {\n    rows := [tr |\n        cells := content_array[_]\n        tds := [td |\n            ctext := cells[_]\n            td := to_cell(ctext)\n        ]\n\n        tr = sprintf(row_tpl, [concat(\"\", tds)])\n    ]\n\n    s := sprintf(table_tpl, [concat(\"\", array.concat([render_table_headers(headers)], rows))])\n}\n\n## why I added it?\nto_cell(txt) = c {\n    c := sprintf(cell_tpl, [txt])\n}\n\nto_colored_text(color, txt) = spn {\n    spn := sprintf(colored_text_tpl, [color, txt])\n}\n\n####################################### Template specific functions #######################################\nto_severity_color(color, level) = spn {\n    spn := to_colored_text(color, format_int(with_default(input.Metadata.vulnerability_summary, level, 0), 10))\n}\n\ncnt_by_severity(severity) = cnt {\n    vln_list := [r |\n        some i, j\n        item := input.Results[i]\n\n        item.Vulnerabilities[j].Severity == severity\n\n        r := item.Vulnerabilities[j]\n    ]\n\n    cnt := count(vln_list)\n}\n\n# 2 dimension array for vulnerabilities summary\nseverities_stats := [\n    [\"critical\", to_severity_color(\"#c00000\", \"critical\")],\n    [\"high\", to_severity_color(\"#e0443d\", \"high\")],\n    [\"medium\", to_severity_color(\"#f79421\", \"medium\")],\n    [\"low\", to_severity_color(\"#e1c930\", \"low\")],\n    [\"unknown\", to_severity_color(\"green\", \"unknown\")],\n]\n\nvlnrb_headers := [\"Layer\", \"Title\",\"Vulnerability ID\", \"Resource name\", \"Path\", \"Installed version\", \"Fix version\", \"Url\"]\n\nrender_vlnrb(severity, list) = sprintf(vlnrb_tpl, [severity, render_table(vlnrb_headers, list)]) {\n    count(list) > 0\n}\n\nrender_vlnrb(severity, list) = \"\" {  #returns empty string if list of vulnerabilities is passed\n    count(list) == 0\n}\n\n# builds 2-dimension array for vulnerability table\nvln_list(severity) = vlnrb {\n    some i, j\n    vlnrb := [r |\n        item := input.Results[i]\n\n        target :=  item.Target\n        vlnname := item.Vulnerabilities[j].VulnerabilityID\n        title := item.Vulnerabilities[j].Title\n        fxvrsn := with_default(item.Vulnerabilities[j], \"FixedVersion\", \"none\")\n        resource_name = with_default(item.Vulnerabilities[j], \"PkgName\", \"none\")\n        resource_path = with_default(item.Vulnerabilities[j], \"PkgPath\", \"none\")\n        resource_version = with_default(item.Vulnerabilities[j], \"InstalledVersion\", \"none\")\n        primaryurl = with_default(item.Vulnerabilities[j], \"PrimaryURL\", \"none\")\n        references = with_default(item.Vulnerabilities[j], \"References\", \"none\")\n\n        item.Vulnerabilities[j].Severity == severity # only items with severity matched\n    r := [target, title, vlnname, resource_name, resource_path, resource_version, fxvrsn, primaryurl]\n    ]\n}\n\n###########################################################################################################\n\ntitle = sprintf(\"%s vulnerability scan report\", [input.ArtifactName])\n\naggregation_pkg := \"postee.vuls.slack.trivy.aggregation\"\n\nresult = msg {\n\n    msg := sprintf(tpl, [\n    input.ArtifactName,\n    render_vlnrb(\"Critical\", vln_list(\"CRITICAL\")),\n    render_vlnrb(\"High\", vln_list(\"HIGH\")),\n    render_vlnrb(\"Medium\", vln_list(\"MEDIUM\")),\n    render_vlnrb(\"Low\", vln_list(\"LOW\")),\n    render_vlnrb(\"Unknown\", vln_list(\"UNKNOWN\"))\n    ])\n}"
  },
  {
    "path": "rego-templates/trivy-operator-dependency-track.rego",
    "content": "package postee.trivyoperator.dependencytrack\n\ntitle:=sprintf(\"%s:%s\", [input.report.artifact.repository, input.report.artifact.tag])\n\nresult:=input.report.components\n"
  },
  {
    "path": "rego-templates/trivy-operator-jira.rego",
    "content": "package postee.trivyoperator.jira\n\nimport data.postee.with_default\n\n################################################ Templates ################################################\n# main template to render message\n\ntpl:=`\nh1. Image: %s in namespace %s\n%s\n%s\n%s\n%s\n%s\n%s\n`\n\nsum_tpl := `\nh4. Summary totals:\n|critical: %s|high: %s|medium: %s|low: %s|unknown: %s|\n`\n\nvlnrb_tpl = `\nh4. %s severity vulnerabilities\n%s\n`\n#Extra % is required in width:100%\n\ntable_tpl := `\n%s\n`\n\ncell_tpl := `| %s `\nheader_tpl := `|| %s `\nrow_tpl_head := `\n%s ||`\nrow_tpl := `\n%s |`\n\ncolored_text_tpl := \"{color:%s}%s{color}\"\n\n############################################## Html rendering #############################################\n\nrender_table_headers(headers) = row {\n\tcount(headers) > 0\n\tths := [th |\n\t\theader := headers[_]\n\t\tth := sprintf(header_tpl, [header])\n\t]\n\n\trow := sprintf(row_tpl_head, [concat(\"\", ths)])\n}\n\nrender_table_headers(headers) = \"\" { #if headers not specified return empty results\n\tcount(headers) == 0\n}\n\nrender_table(headers, content_array) = s {\n\trows := [tr |\n\t\tcells := content_array[_]\n\t\ttds := [td |\n\t\t\tctext := cells[_]\n\t\t\ttd := to_cell(ctext)\n\t\t]\n\n\t\ttr = sprintf(row_tpl, [concat(\"\", tds)])\n\t]\n\n\ts := sprintf(table_tpl, [concat(\"\", array.concat([render_table_headers(headers)], rows))])\n}\n\n## why I added it?\nto_cell(txt) = c {\n\tc := sprintf(cell_tpl, [txt])\n}\n\nto_colored_text(color, txt) = spn {\n\tspn := sprintf(colored_text_tpl, [color, txt])\n}\n\n####################################### Template specific functions #######################################\nto_severity_color(color, level) = spn {\n\tspn := to_colored_text(color, format_int(with_default(input.report.summary,level, 0), 10))\n}\n\nrender_image_name := sprintf(\"%s:%s\", [\n\twith_default(input.report.artifact,\"repository\",\"unknown\"),\n\twith_default(input.report.artifact,\"tag\",\"unknown\")\n])\n\nrender_summary := sprintf(sum_tpl,[\n\tto_severity_color(\"#c00000\", \"criticalCount\"),\n\tto_severity_color(\"#e0443d\", \"highCount\"),\n\tto_severity_color(\"#f79421\", \"mediumCount\"),\n\tto_severity_color(\"#e1c930\", \"lowCount\"),\n\tto_severity_color(\"#505f79\", \"unknownCount\")\n])\n\nvlnrb_headers := [\"ID\",\"Title\", \"Resource\", \"Installed version\", \"Fixed version\", \"Url\"]\n\nrender_vlnrb(severity, list) = sprintf(vlnrb_tpl, [severity, render_table(vlnrb_headers, list)]) {\n\tcount(list) > 0\n}\n\nrender_vlnrb(severity, list) = \"\" {  #returns empty string if list of vulnerabilities is passed\n\tcount(list) == 0\n}\n\n# builds 2-dimension array for vulnerability table\nvln_list(severity) = vlnrb {\n\tsome j\n\tvlnrb := [r |\n\t\titem := input.report.vulnerabilities[j]\n\t\tvlnname := item.vulnerabilityID\n\t\ttitle := item.title\n\t\tfxvrsn := with_default(item, \"fixedVersion\", \"none\")\n\t\tresource = with_default(item, \"resource\", \"none\")\n\t\tresource_version = with_default(item, \"installedVersion\", \"none\")\n\t\tprimaryurl = with_default(item, \"primaryLink\", \"none\")\n\n\t\titem.severity == severity # only items with severity matched\n\tr := [vlnname, title, resource, resource_version, fxvrsn, primaryurl]\n\t]\n}\n\n###########################################################################################################\n\ntitle = sprintf(\"Vulnerability issue with image %s in namespace %s\", [render_image_name, with_default(input.metadata,\"namespace\",\"unknown\")])\nresult = msg {\n\tmsg := sprintf(tpl, [\n\trender_image_name,\n\twith_default(input.metadata,\"namespace\",\"unknown\"),\n\trender_summary,\n\trender_vlnrb(\"Critical\", vln_list(\"CRITICAL\")),\n\trender_vlnrb(\"High\", vln_list(\"HIGH\")),\n\trender_vlnrb(\"Medium\", vln_list(\"MEDIUM\")),\n\trender_vlnrb(\"Low\", vln_list(\"LOW\")),\n\trender_vlnrb(\"Unknown\", vln_list(\"UNKNOWN\"))\n\t])\n}\n"
  },
  {
    "path": "rego-templates/trivy-operator-slack.rego",
    "content": "package postee.trivyoperator.slack\n\nimport data.postee.flat_array #converts [[{...},{...}], [{...},{...}]] to [{...},{...},{...},{...}]\nimport data.postee.with_default\n\n############################################# Common functions ############################################\n\n# render_sections split collection of cells provided to chunks of 5 rows each and wraps every chunk with section element\nrender_sections(rows, caption, headers) = result {\n\tcount(rows) > 0 # only if some vulnerabilities are found\t\n\trows_and_header := array.concat(headers, rows)\n\ta := flat_array([s |\n\t\t# code below converts 2 dimension array like [[row1, row2, ... row5], ....]\n\t\tgroup_size := 10 #it's 5 but every row is represented by 2 items\n\t\tnum_chunks := ceil(count(rows_and_header) / group_size) - 1\n\t\tindices := {b | b := numbers.range(0, num_chunks)[_] * group_size}\n\t\tsome k\n\t\tfields := [array.slice(rows_and_header, i, i + group_size) | i := indices[_]][k]\n\t\t# builds markdown section based on slice\n\n    \ts := with_caption(fields, caption, k)\n\t])\n\tresult := array.concat(a, [{\"type\": \"divider\"}])\n}\n\nrender_sections(rows, caption, headers) = [] { #do not render section if provided collection is empty\n\tcount(rows) == 0\n}\n\nwith_caption(fields, caption, position) = s {\n    position == 0\n\ts := [\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": caption,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"fields\": fields,\n\t\t},\n\t]\n}\nwith_caption(fields, caption, position) = s {\n    position > 0\n\ts := [\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"fields\": fields,\n\t\t},\n\t]\n}\n\n\n###########################################################################################################\n\nvln_list(severity) = l {\n\t# builds list of rows for section for the given severity\n    some i\n\tvlnrb := [r |\n\t\titem := input.report.vulnerabilities[i]\n\t\tvlnname := item.vulnerabilityID\n\n\t\tfxvrsn := with_default(item, \"fixedVersion\", \"none\")\n\t\tresource_name = with_default(item, \"resource\", \"none\")\n\t\tresource_version = with_default(item, \"installedVersion\", \"none\")\n\t\turl = with_default(item, \"primaryLink\",\"\")\n\t\titem.severity == severity # only items with severity matched\n\n\t\tr := [\n\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"<%s|%s>\",[url,vlnname])},\n\t\t\t{\"type\": \"mrkdwn\", \"text\": concat(\" / \", [resource_name, resource_version, fxvrsn])},\n\t\t]\n\t]\n\n\tcaption := sprintf(\"*%s severity vulnerabilities*\", [severity]) #TODO make first char uppercase\n\n\theaders := [\n\t\t{\"type\": \"mrkdwn\", \"text\": \"*Vulnerability ID*\"},\n\t\t{\"type\": \"mrkdwn\", \"text\": \"*Resource / Version / Fixed version*\"},\n\t]\n\n\t# split rows and wrap slices with markdown section\n\tl := render_sections(flat_array(vlnrb), caption, headers)\n}\n\nimage_name := sprintf(\"%s:%s\", [\n\twith_default(input.report.artifact,\"repository\",\"unknown\"),\n\twith_default(input.report.artifact,\"tag\",\"unknown\")\n])\n###########################################################################################################\npostee := with_default(input, \"postee\", {})\n\ntitle = sprintf(\"Vulnerability scan report %s\", [image_name]) # title is \n\nresult = res {\n\n\theader := [\n\t\t{\n\t\t\t\"type\": \"header\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"plain_text\",\n\t\t\t\t\"text\": sprintf(\"Vulnerability issue with image:%s in namespace %s\",[image_name, with_default(input.metadata,\"namespace\",\"unknown\")]),\n\t\t\t},\n\t\t}\n\t]\n\n\tsummary := [\n\t\t{\n\t\t\t\"type\": \"divider\"\n\t\t},\n\t\t{\n\t\t\t\"type\": \"context\",\n\t\t\t\"elements\": [\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": \"*Summary totals:*\"},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\t\"type\": \"context\",\n\t\t\t\"elements\": [\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"Critical: *%d*\", [input.report.summary.criticalCount])},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"High: *%d*\", [input.report.summary.highCount])},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"Medium: *%d*\", [input.report.summary.mediumCount])},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"Low: *%d*\", [input.report.summary.lowCount])},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"Unknown: *%d*\", [input.report.summary.unknownCount])},\n\t\t\t],\n\t\t},\n\t\t{\n\t\t\t\"type\": \"divider\"\n\t\t}\n\t]\n\n\tres := flat_array([\n\t\theader,\n\t\tsummary,\n\t\tvln_list(\"CRITICAL\"),\n\t\tvln_list(\"HIGH\"),\n\t\tvln_list(\"MEDIUM\"),\n\t\tvln_list(\"LOW\"),\n\t\tvln_list(\"UNKNOWN\")\n\t])\n}\n"
  },
  {
    "path": "rego-templates/trivy-vulns-slack.rego",
    "content": "package postee.vuls.trivy.slack\n\nimport data.postee.by_flag\nimport data.postee.duplicate\nimport data.postee.flat_array #converts [[{...},{...}], [{...},{...}]] to [{...},{...},{...},{...}]\nimport data.postee.with_default\n\n############################################# Common functions ############################################\n\n# TODO support generic property\ncheck_failed(item) = false {\n\tnot item.failed\n}\n\ncheck_failed(item) {\n\titem.failed\n}\n\n###########################################################################################################\n\n# render_sections split collection of cells provided to chunks of 5 rows each and wraps every chunk with section element\nrender_sections(rows, caption, headers) = a {\n\tcount(rows) > 0 # only if some vulnerabilities are found\n\trows_and_header := array.concat(headers, rows)\n\ta := flat_array([s |\n\t\t# code below converts 2 dimension array like [[row1, row2, ... row5], ....]\n\t\tgroup_size := 10 #it's 5 but every row is represented by 2 items\n\t\tnum_chunks := ceil(count(rows) / group_size) - 1\n\t\tindices := {b | b := numbers.range(0, num_chunks)[_] * group_size}\n\t\tsome k\n\t\tfields := [array.slice(rows_and_header, i, i + group_size) | i := indices[_]][k]\n\t\t# builds markdown section based on slice\n\n    \ts := with_caption(fields, caption, k)\n\t])\n\n}\n\nrender_sections(rows, caption, headers) = [] { #do not render section if provided collection is empty\n\tcount(rows) == 0\n}\n\nwith_caption(fields, caption, position) = s {\n    position == 0\n\ts := [\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": caption,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"fields\": fields,\n\t\t},\n\t]\n}\nwith_caption(fields, caption, position) = s {\n    position > 0\n\ts := [\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"fields\": fields,\n\t\t},\n\t]\n}\n\n\n###########################################################################################################\n\nvln_list(severity) = l {\n\t# builds list of rows for section for the given severity\n\tvlnrb := [r |\n\t\tsome i, j\n\t\titem := input.Results[i]\n\t\tvlnname := item.Vulnerabilities[j].VulnerabilityID\n\n\t\tfxvrsn := with_default(item.Vulnerabilities[j], \"FixedVersion\", \"none\")\n\t\tresource_name = with_default(item.Vulnerabilities[j], \"PkgName\", \"none\")\n\t\tresource_version = with_default(item.Vulnerabilities[j], \"InstalledVersion\", \"none\")\n\n\t\titem.Vulnerabilities[j].Severity == severity\n\n\t\tr := [\n\t\t\t{\"type\": \"mrkdwn\", \"text\": vlnname},\n\t\t\t{\"type\": \"mrkdwn\", \"text\": concat(\" / \", [resource_name, resource_version, fxvrsn])},\n\t\t]\n\t]\n\n\tcaption := sprintf(\"*%s severity vulnerabilities*\", [severity]) #TODO make first char uppercase\n\n\theaders := [\n\t\t{\"type\": \"mrkdwn\", \"text\": \"*Vulnerability ID*\"},\n\t\t{\"type\": \"mrkdwn\", \"text\": \"*Resource name / Installed version / Fix version*\"},\n\t]\n\n\t# split rows and wrap slices with markdown section\n\tl := render_sections(flat_array(vlnrb), caption, headers)\n}\n\ncnt_by_severity(severity) = cnt {\n\tvln_list := [r |\n\t\tsome i, j\n\t\titem := input.Results[i]\n\n\t\titem.Vulnerabilities[j].Severity == severity\n\n\t\tr := item.Vulnerabilities[j]\n\t]\n\n\tcnt := count(vln_list)\n}\n\n###########################################################################################################\npostee := with_default(input, \"postee\", {})\n\naqua_server := with_default(postee, \"AquaServer\", \"\")\n\ntitle = sprintf(\"Vulnerability scan report\", []) # title is \n\naggregation_pkg := \"postee.vuls.slack.trivy.aggregation\"\n\nresult = res {\n\tseverities := [\"CRITICAL\", \"HIGH\", \"MEDIUM\", \"LOW\", \"UNKNOWN\"]\n\n\theaders := [\n\t\t{\"type\": \"section\", \"text\": {\"type\": \"mrkdwn\", \"text\": sprintf(\"Artifact name: %s\", [input.ArtifactName])}},\n\t\t{\"type\": \"section\", \"text\": {\"type\": \"mrkdwn\", \"text\": sprintf(\"Type: %s\", [input.ArtifactType])}},\n\t\t{\n\t\t\t\"type\": \"section\",\n\t\t\t\"text\": {\n\t\t\t\t\"type\": \"mrkdwn\",\n\t\t\t\t\"text\": \"*Found vulnerabilities*\",\n\t\t\t},\n\t\t},\n\t]\n\n\tsummary:= [\n\t    {\n    \t\t\"type\": \"section\",\n    \t    \"text\": {\n    \t\t\t\"type\": \"mrkdwn\",\n    \t\t\t\"text\": \"*Found vulnerabilities*\",\n    \t\t},\n    \t},\n    ]\n\n\tres := flat_array([\n\t\theaders,\n\t\tvln_list(\"CRITICAL\"),\n\t\tvln_list(\"HIGH\"),\n\t\tvln_list(\"MEDIUM\"),\n\t\tvln_list(\"LOW\"),\n\t\tvln_list(\"UNKNOWN\"),\n\t\tsummary,\n\t\t[{\n\t\t\t\"type\": \"section\",\n\t\t\t\"fields\": [\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": \"Critical\"},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"*%d*\", [cnt_by_severity(\"CRITICAL\")])},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": \"High\"},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"*%d*\", [cnt_by_severity(\"HIGH\")])},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": \"Medium\"},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"*%d*\", [cnt_by_severity(\"MEDIUM\")])},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": \"Low\"},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"*%d*\", [cnt_by_severity(\"LOW\")])},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": \"Unknown\"},\n\t\t\t\t{\"type\": \"mrkdwn\", \"text\": sprintf(\"*%d*\", [cnt_by_severity(\"UNKNOWN\")])},\n\t\t\t],\n\t\t}],\n\t])\n}"
  },
  {
    "path": "rego-templates/trivy-vuls-slack-aggregation.rego",
    "content": "package postee.vuls.slack.trivy.aggregation\n\nimport data.postee.flat_array\n\n\ntitle := \"Vulnerability scan report\"\n\nurl := urlsResult {\n    urls := [ scan | \n            item:=input[i].PrimaryURL\n\n            scan:=[item]\n    ] \n\n    urlsResult:= concat(\"\\n\", flat_array(urls))\n}\n\nresult := res {\n    scans := [ scan | \n            item:=input[i].Description #collection is expected\n\n            scan:=array.concat([{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\": input[i].title}}], item)\n    ] \n\n    res:= flat_array(scans)\n}\n\n\n"
  },
  {
    "path": "rego-templates/vuls-cyclonedx.rego",
    "content": "package postee.vuls.cyclondx\n\nimport data.postee.with_default\n\n\nbom_tpl:=`<?xml version=\"1.0\"?>\n<bom serialNumber=\"urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79\" version=\"1\"\n     xmlns=\"http://cyclonedx.org/schema/bom/1.1\"\n     xmlns:v=\"http://cyclonedx.org/schema/ext/vulnerability/1.0\">\n  <components>\n  %s\n  </components>\n</bom>`\n\ncomponent_tpl:=`    <component type=\"application\">\n      <name>%s</name>\n      <version>%s</version>\n      <licenses>\n        <license>\n          <id>%s</id>\n        </license>\n      </licenses>\n      %s\n    </component>`\n\nvlnrb_tpl := `\n        <v:vulnerability>\n          <v:id>%s</v:id> \n          <v:source name=\"NVD\">\n            <v:url>%s</v:url>\n          </v:source>\n          <v:ratings>\n            <v:rating>\n              <v:score>\n                <v:base>%v</v:base>\n                <v:impact>%v</v:impact>\n                <v:exploitability>%v</v:exploitability>\n              </v:score>\n              <v:severity>%s</v:severity>\n              <v:method>%s</v:method>\n              <v:vector>%s</v:vector>\n            </v:rating>\n          </v:ratings>\n          <v:recommendations>\n            <v:recommendation>%s</v:recommendation>\n          </v:recommendations>\n        </v:vulnerability>`\nvlnrb_lst_tpl := `<v:vulnerabilities>%s</v:vulnerabilities>`\n\nrender_vlnrb(vlnrb_lst) = xml {\n\tl := [r |\n        vlnrb := vlnrb_lst[_]\n        vln_name := vlnrb.name\n        nvd_url := vlnrb.nvd_url\n        # description is skipped\n        vln_severity := vlnrb.aqua_severity\n        vln_method := vlnrb.aqua_scoring_system\n        vln_vectors := vlnrb.aqua_vectors\n        vln_score := vlnrb.aqua_score\n        vln_solution := with_default(vlnrb, \"solution\", \"No solution available\")\n\n        r := sprintf(vlnrb_tpl, [vln_name, nvd_url, vln_score, vln_score, vln_score, vln_severity, vln_method, vln_vectors, vln_solution])\n    ]\n\n    xml := sprintf(vlnrb_lst_tpl, [concat(\"\", l)])\n}\n\nrender_components := l {\n\tl := [r |\n                    item := input.resources[_]\n\n                    component := item.resource\n                    component_name := with_default(component, \"name\", \"none\")\n                    component_version := with_default(component, \"version\", \"none\")\n                    # nexus iq has db limit for license field\n                    component_license := substring(with_default(component, \"license\", \"not provided\"), 0, 32)\n\n                    vlnrb:=render_vlnrb(item.vulnerabilities)\n\n\n                    r := sprintf(component_tpl, [component_name, component_version, component_license, vlnrb])\n              ]\n}\n\ntitle := input.image\n\nresult := sprintf(bom_tpl, [concat(\"\",render_components)])\n\n"
  },
  {
    "path": "rego-templates/vuls-html-aggregation.rego",
    "content": "package postee.vuls.html.aggregation\n\nimport data.postee.flat_array\n\n\ntitle := \"Vulnerability scan report\"\n\nurl := urlsResult {\n    urls := [ scan | \n            item:=input[i].url\n\n            scan:=[item]\n    ] \n\n    urlsResult:= concat(\"\\n\", flat_array(urls))\n}\n\n\nresult := res {\n    scans := [ scan | \n            item:=input[i].description\n\n            scan:=[sprintf(\"<h1>%s</h1>\", [input[i].title]), item]\n    ] \n\n    res:= concat(\"\\n\", flat_array(scans))\n}\n\n\n"
  },
  {
    "path": "rego-templates/vuls-html.rego",
    "content": "package postee.vuls.html\n\nimport data.postee.by_flag\nimport data.postee.with_default\n\n#import common.by_flag\n################################################ Templates ################################################\n#main template to render message\ntpl:=`\n<p>Image name: %s</p>\n<p>Registry: %s</p>\n<p>%s</p>\n<p>%s</p>\n<p>%s</p>\n<!-- stats -->\n%s\n<h2>Assurance controls</h2>\n%s\n<!-- Critical severity vulnerabilities -->\n%s\n<!-- High severity vulnerabilities -->\n%s\n<!-- Medium severity vulnerabilities -->\n%s\n<!-- Low severity vulnerabilities -->\n%s\n<!-- Negligible severity vulnerabilities -->\n%s\n%s\n`\n\nvlnrb_tpl = `\n<h3>%s severity vulnerabilities</h3>\n%s\n`\n#Extra % is required in width:100%\n\ntable_tpl:=`\n<TABLE border='1' style='width: 100%%; border-collapse: collapse;'>\n%s\n</TABLE>\n`\n\ncell_tpl:=`<TD style='padding: 5px;'>%s</TD>\n`\n\nheader_tpl:=`<TH style='padding: 5px;'>%s</TH>\n`\n\nrow_tpl:=`\n<TR>\n%s\n</TR>`\n\ncolored_text_tpl:=\"<span style='color:%s'>%s</span>\"\n\n###########################################################################################################\n\n############################################## Html rendering #############################################\n\nrender_table_headers(headers) = row {\n    count(headers) > 0\n    ths := [th |\n        header := headers[_]\n        th := sprintf(header_tpl, [header])\n    ]\n\n    row := sprintf(row_tpl, [concat(\"\", ths)])\n}\n\n\nrender_table_headers(headers) = \"\" { #if headers not specified return empty results\n    count(headers) == 0\n}\n\n\nrender_table(headers, content_array) = s {\n\trows := [tr |\n    \t\t\tcells:=content_array[_]\n    \t\t\ttds:= [td |\n                \tctext:=cells[_]\n                    td := to_cell(ctext)\n                ]\n                tr=sprintf(row_tpl, [concat(\"\", tds)])\n    \t\t]\n\n\ts:=sprintf(table_tpl, [concat(\"\", array.concat([render_table_headers(headers)],rows))])\n}\n\n## why I added it?\nto_cell(txt) = c {\n    c:= sprintf(cell_tpl, [txt])\n}\n\nto_colored_text(color, txt) = spn {\n    spn :=sprintf(colored_text_tpl, [color, txt])\n}\n\n####################################### Template specific functions #######################################\nto_severity_color(color, level) = spn {\n spn:=to_colored_text(color, format_int(with_default(input.vulnerability_summary,level,0), 10))\n}\n# TODO refactor to support different properties\ncheck_failed(item) = false {\nnot item.failed #Either absent or false\n}\ncheck_failed(item) = true {\n item.failed\n}\n\n# 2 dimension array for vulnerabilities summary\nseverities_stats := [\n                        [\"critical\", to_severity_color(\"#c00000\", \"critical\")],\n                        [\"high\", to_severity_color(\"#e0443d\", \"high\")],\n                        [\"medium\", to_severity_color(\"#f79421\", \"medium\")],\n                        [\"low\", to_severity_color(\"#e1c930\", \"low\")],\n                        [\"negligible\", to_severity_color(\"green\", \"negligible\")]\n                    ]\n\n# 2 dimension array for assurance controls\nassurance_controls := [ control |\n                    item := input.image_assurance_results.checks_performed[i]\n                    control := [format_int(i+1, 10), item.control,item.policy_name,\n                                            by_flag(\n                                                \"FAIL\",\n                                                \"PASS\",\n                                                check_failed(item)\n                                            )\n                    ]\n]\n\nvlnrb_headers := [\"Vulnerability ID\", \"Resource name\", \"Installed version\", \"Fix version\"]\n\n\nrender_vlnrb(severity, list) = sprintf(vlnrb_tpl, [severity, render_table(vlnrb_headers, list)]) {\n    count(list) > 0\n}\n\nrender_vlnrb(severity, list) = \"\" {  #returns empty string if list of vulnerabilities is passed\n    count(list) == 0\n}\n\n# builds 2-dimension array for vulnerability table\nvln_list(severity) = vlnrb {\n    some i, j\n\tvlnrb := [r |\n                    item := input.resources[i]\n\n\n                    resource := item.resource\n                    vlnname := item.vulnerabilities[j].name\n                    fxvrsn := with_default(item.vulnerabilities[j],\"fix_version\", \"none\")\n                    resource_name = with_default(resource, \"name\", \"none\")\n                    resource_version = with_default(resource, \"version\", \"none\")\n\n                    item.vulnerabilities[j].aqua_severity == severity # only items with severity matched\n                    r := [vlnname, resource_name, resource_version, fxvrsn]\n              ]\n}\n###########################################################################################################\npostee := with_default(input, \"postee\", {})\naqua_server := with_default(postee, \"AquaServer\", \"\")\n\ntitle = sprintf(\"%s vulnerability scan report\", [input.image])\nhref := sprintf(\"%s%s/%s\", [aqua_server, urlquery.encode(input.registry), urlquery.encode(input.image)])\ntext := sprintf(\"%s%s/%s\", [aqua_server, input.registry, input.image])\nurl := by_flag(\"\", href, aqua_server == \"\")\n\naggregation_pkg := \"postee.vuls.html.aggregation\"\nresult = msg {\n\n    msg := sprintf(tpl, [\n    input.image,\n    input.registry,\n\tby_flag(\n     \"Image is non-compliant\",\n     \"Image is compliant\",\n     with_default(input.image_assurance_results, \"disallowed\", false)\n    ),\n\tby_flag(\n     \"Malware found: Yes\",\n     \"Malware found: No\",\n     input.scan_options.scan_malware #reflects current logic\n    ),\n\tby_flag(\n\t \"Sensitive data found: Yes\",\n     \"Sensitive data found: No\",\n     input.scan_options.scan_sensitive_data #reflects current logic\n\t),\n    render_table([], severities_stats),\n    render_table([\"#\",\"Control\",\"Policy Name\", \"Status\"], assurance_controls),\n\n    render_vlnrb(\"Critical\", vln_list(\"critical\")),\n    render_vlnrb(\"High\", vln_list(\"high\")),\n    render_vlnrb(\"Medium\", vln_list(\"medium\")),\n    render_vlnrb(\"Low\", vln_list(\"low\")),\n    render_vlnrb(\"Negligible\", vln_list(\"negligible\")),\n\n    by_flag(\n     \"\", \n     sprintf(`<p>See more: <a href='%s'>%s</a></p>`,[href, text]), #link\n     aqua_server == \"\")\n    ])\n}\n"
  },
  {
    "path": "rego-templates/vuls-opsgenie.rego",
    "content": "package postee.vuls.opsgenie\n\ntitle = input.image\n\nresult= {\n \"description\":sprintf(\"%s vulnerability scan report\", [input.image]),\n \"alias\":input.image\n}"
  },
  {
    "path": "rego-templates/vuls-slack-aggregation.rego",
    "content": "package postee.vuls.slack.aggregation\n\nimport data.postee.flat_array\n\n\ntitle := \"Vulnerability scan report\"\n\nurl := urlsResult {\n    urls := [ scan | \n            item:=input[i].url\n\n            scan:=[item]\n    ] \n\n    urlsResult:= concat(\"\\n\", flat_array(urls))\n}\n\nresult := res {\n    scans := [ scan | \n            item:=input[i].description #collection is expected\n\n            scan:=array.concat([{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\": input[i].title}}], item)\n    ] \n\n    res:= flat_array(scans)\n}\n\n\n"
  },
  {
    "path": "rego-templates/vuls-slack.rego",
    "content": "package postee.vuls.slack\n\nimport data.postee.by_flag\nimport data.postee.flat_array #converts [[{...},{...}], [{...},{...}]] to [{...},{...},{...},{...}]\nimport data.postee.duplicate\nimport data.postee.with_default\n\n############################################# Common functions ############################################\n\n# TODO support generic property\ncheck_failed(item) = false {\nnot item.failed\n}\ncheck_failed(item) = true {\n item.failed\n}\n###########################################################################################################\n\n# render_sections split collection of cells provided to chunks of 5 rows each and wraps every chunk with section element\nrender_sections(rows, caption) = a { \n    count(rows) > 0 # only if some vulnerabilities are found\n    a:=flat_array([ s |\n        # code below converts 2 dimension array like [[row1, row2, ... row5], ....]\n        group_size := 10 #it's 5 but every row is represented by 2 items\n        num_chunks := ceil(count(rows) / group_size) - 1\n        indices := { b | b := numbers.range(0, num_chunks)[_] * group_size }\n    \tfields:=[array.slice(rows, i, i + group_size) | i := indices[_]][_]\n\n        # builds markdown section based on slice\n\n        s := [\n        \t{\n                \"type\": \"section\",\n                \"text\": {\n                    \"type\": \"mrkdwn\",\n                    \"text\": caption\n                }\n            },\n            {\n                \"type\": \"section\",\n                \"fields\":fields\n\n            }\n        ]\n\t])\n}\nrender_sections(rows, caption) = [] { #do not render section if provided collection is empty\n    count(rows) == 0\n}\n###########################################################################################################\n\n\nvln_list(severity) = l {\n    # builds list of rows for section for the given severity\n\tvlnrb := [r |\n                    some i, j\n                    item := input.resources[i]\n                    resource := item.resource\n                    vlnname := item.vulnerabilities[j].name\n\n                    fxvrsn := with_default(item.vulnerabilities[j],\"fix_version\", \"none\")\n                    resource_name = with_default(resource, \"name\", \"none\")\n                    resource_version = with_default(resource, \"version\", \"none\")\n\n                    item.vulnerabilities[j].aqua_severity == severity\n\n                    r := [\n                    \t{\"type\": \"mrkdwn\", \"text\": vlnname},\n                    \t{\"type\": \"mrkdwn\", \"text\": concat(\"/\", [resource_name, resource_version, fxvrsn])}\n                    ]\n\n              ]\n    caption := sprintf(\"*%s severity vulnerabilities*\", [severity])  #TODO make first char uppercase\n    \n\n    headers := [\n        {\"type\": \"mrkdwn\", \"text\": \"*Vulnerability ID*\"},\n        {\"type\": \"mrkdwn\", \"text\": \"*Resource name / Installed version / Fix version*\"}\n    ]\n    rows := array.concat(headers, flat_array(vlnrb))\n\n    # split rows and wrap slices with markdown section\n    l := render_sections(rows, caption)\n}\nmalware_list := l {\n\tmlwr := [r |\n    \t\t\t\titem := input.malware[i]\n\n                    r := [\n                    \t{\"type\": \"mrkdwn\", \"text\": sprintf(\"%d %s\", [i+1, item.malware])},\n                    \t{\"type\": \"mrkdwn\", \"text\": concat(\"/\", [item.hash, item.path])}\n                    ]\n\n              ]\n\n    headers := [\n        {\"type\": \"mrkdwn\", \"text\": \"*# Malware*\"},\n        {\"type\": \"mrkdwn\", \"text\": \"*Hash / Path*\"}\n    ]\n    rows := array.concat(headers, flat_array(mlwr))\n\n    # split rows and wrap slices with markdown section\n    l := render_sections(rows, \"Malware\")\n}\n\n###########################################################################################################\npostee := with_default(input, \"postee\", {})\naqua_server := with_default(postee, \"AquaServer\", \"\")\n\ntitle = sprintf(\"%s vulnerability scan report\", [input.image]) # title is string\nhref:=sprintf(\"%s%s/%s\", [aqua_server, urlquery.encode(input.registry), urlquery.encode(input.image)])\ntext:=sprintf(\"%s%s/%s\", [aqua_server, input.registry, input.image])\nurl := by_flag(\"\", href, aqua_server == \"\")\n\naggregation_pkg := \"postee.vuls.slack.aggregation\"\n\nresult = res {\n\tseverities := [\"critical\", \"high\", \"medium\", \"low\", \"negligible\"]\n\n\tchecks_performed:= flat_array([check |\n                    item := input.image_assurance_results.checks_performed[i]\n                    check:= [\n                        {\"type\": \"mrkdwn\", \"text\": sprintf(\"%d %s\", [i+1, item.control])},\n                        {\"type\": \"mrkdwn\", \"text\": concat(\" / \", [item.policy_name, by_flag(\"FAIL\", \"PASS\", check_failed(item))])}\n                    ]\n\n    ])\n\n    severity_stats:= flat_array([gr |\n            severity := severities[_]\n            gr:= [\n                {\"type\": \"mrkdwn\", \"text\": sprintf(\"*%s*\", [upper(severity)])},\n                {\"type\": \"mrkdwn\", \"text\": sprintf(\"*%d*\", [input.vulnerability_summary[severity]])},\n            ]            \n    ])\n\n\n\theaders := [{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":sprintf(\"Image name: %s\", [input.image])}},\n    \t\t\t{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":sprintf(\"Registry: %s\", [input.registry])}},\n    \t\t\t{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\": by_flag(\n                                                                        \"Image is non-compliant\",\n                                                                        \"Image is compliant\",\n                                                                        with_default(input.image_assurance_results, \"disallowed\", false)\n                                                                    )}},\n    \t\t\t{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\": by_flag(\n                                                                        \"Malware found: Yes\",\n                                                                        \"Malware found: No\",\n                                                                        input.scan_options.scan_malware #reflects current logic\n                                                                    )}},\n    \t\t\t{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\": by_flag(\n                                                                        \"Sensitive data found: Yes\",\n                                                                        \"Sensitive data found: No\",\n                                                                        input.scan_options.scan_sensitive_data #reflects current logic\n                                                                    )}},\n                {\n                \"type\": \"section\",\n                \"fields\": severity_stats\n                },\n                {\n                    \"type\": \"section\",\n                    \"text\": {\n                        \"type\": \"mrkdwn\",\n                        \"text\": \"*Assurance controls*\"\n                    }\n                },\n                {\n                \"type\": \"section\",\n                \"fields\": array.concat(\n                    [{\n                        \"type\": \"mrkdwn\",\n                        \"text\": \"*#* *Control*\"\n                    },\n                    {\n                        \"type\": \"mrkdwn\",\n                        \"text\": \"*Policy Name* / *Status*\"\n                    }], checks_performed)\n                },\n                {\n                    \"type\": \"section\",\n                    \"text\": {\n                        \"type\": \"mrkdwn\",\n                        \"text\": \"*Found vulnerabilities*\"\n                    }\n                }\n\t           ]\n\n    urlText :=sprintf(\"See more: \\u003c%s|%s\\u003e\", [href, text])\n\n    footers := by_flag(\n        \"\", \n        [{\n            \"type\": \"section\",\n            \"text\": {\n                \"type\": \"mrkdwn\",\n                \"text\": urlText\n            }\n        }], \n        aqua_server == \"\")\n\n    res := flat_array([\n        headers,\n        vln_list(\"critical\"), \n        vln_list(\"high\"),\n        vln_list(\"medium\"),\n        vln_list(\"low\"),\n        vln_list(\"negligible\"),\n        malware_list,\n        footers\n    ])\n\n}\n\n\n"
  },
  {
    "path": "regoservice/aggregation_test.go",
    "content": "package regoservice\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n)\n\nvar (\n\tregoWithAggregation = `\npackage rego1\ntitle:=\"Audit event received\"\nresult:=sprintf(\"Audit event received from %s\", [input.user])\naggregation_pkg:=\"rego1.aggr\"\n`\n\taggregationRego = `\npackage rego1.aggr\nimport data.postee.flat_array\n\n\ntitle := \"Vulnerability scan report\"\nresult := res {\n    scans := [ scan | \n            item:=input[i].description\n\n            scan:=[sprintf(\"<h1>%s</h1>\", [input[i].title]), item]\n    ] \n\n    res:= concat(\"\\n\", flat_array(scans))\n}\n`\n\n\tcommonRego = `package postee\nflat_array(a) = o {\n\to:=[item |\n\t\titem:=a[_][_]\n\t]\n}\t\n`\n)\n\nfunc TestAggregation(t *testing.T) {\n\ttests := []struct {\n\t\tregoRule            *string\n\t\taggregationRegoRule *string\n\t\tcaseDesc            string\n\t\titems               []map[string]string\n\t\tregoPackage         string\n\t\texpectedValues      map[string]string\n\t}{\n\t\t{\n\t\t\tregoRule:            &regoWithAggregation,\n\t\t\taggregationRegoRule: &aggregationRego,\n\t\t\tcaseDesc:            \"simple case\",\n\t\t\titems: []map[string]string{{\n\t\t\t\t\"title\":       \"title1\",\n\t\t\t\t\"description\": \"description1\",\n\t\t\t}, {\n\t\t\t\t\"title\":       \"title2\",\n\t\t\t\t\"description\": \"description2\",\n\t\t\t}},\n\t\t\tregoPackage: \"rego1\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"Vulnerability scan report\",\n\t\t\t\t\"description\": `<h1>title1</h1>\ndescription1\n<h1>title2</h1>\ndescription2`,\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\taggregateBuildinRego(t, test.regoRule, test.aggregationRegoRule, test.items, test.regoPackage, test.expectedValues)\n\t}\n}\n\nfunc aggregateBuildinRego(t *testing.T, regoRule *string, aggregationRegoRule *string, items []map[string]string, regoPackage string, expectedValues map[string]string) {\n\tbuildinRegoTemplatesSaved := buildinRegoTemplates\n\ttestRego := \"rego1.rego\"\n\taggrRego := \"aggr1.rego\"\n\tcommonRegoFilename := \"common.rego\"\n\tbuildinRegoTemplates = []string{commonRegoFilename, testRego, aggrRego} //common part goes in single bundle\n\n\terr := ioutil.WriteFile(commonRegoFilename, []byte(commonRego), 0644)\n\terr = ioutil.WriteFile(testRego, []byte(*regoRule), 0644)\n\terr = ioutil.WriteFile(aggrRego, []byte(*aggregationRegoRule), 0644)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tdefer func() {\n\t\tbuildinRegoTemplates = buildinRegoTemplatesSaved\n\t\tos.Remove(testRego)\n\t\tos.Remove(commonRegoFilename)\n\t\tos.Remove(aggrRego)\n\t}()\n\tdemo, err := BuildBundledRegoEvaluator(regoPackage)\n\tif err != nil {\n\t\tt.Errorf(\"received an unexpected error: %v\\n\", err)\n\t}\n\tif !demo.IsAggregationSupported() {\n\t\tt.Errorf(\"Should support aggregation\")\n\t\treturn\n\t}\n\tr, err := demo.BuildAggregatedContent(items)\n\tif err != nil {\n\t\tt.Errorf(\"received an unexpected error: %v\\n\", err)\n\t}\n\n\tfor key, expected := range expectedValues {\n\t\tif r[key] != expected {\n\t\t\tt.Errorf(\"Incorrect %s: expected %s, got %s\\n\", key, expected, r[key])\n\t\t}\n\n\t}\n}\n"
  },
  {
    "path": "regoservice/eval.go",
    "content": "package regoservice\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/open-policy-agent/opa/rego\"\n\t\"io/fs\"\n\t\"log\"\n)\n\nconst (\n\tresult_prop          = \"result\"\n\ttitle_prop           = \"title\"\n\turl_prop             = \"url\"\n\taggregation_pkg_prop = \"aggregation_pkg\"\n\n\t//ServiceNow props\n\tdateProp          = \"result_date\"\n\tseverityProp      = \"result_severity\"\n\tcategoryProp      = \"result_category\"\n\tsubcategoryProp   = \"result_subcategory\"\n\tassignedToProp    = \"result_assigned_to\"\n\tassignedGroupProp = \"result_assigned_group\"\n\tsummaryProp       = \"result_summary\"\n)\n\nvar (\n\tbuildinRegoTemplates = []string{\"./rego-templates\"}\n\tcommonRegoTemplates  = []string{\"./rego-templates/common\"}\n)\n\ntype regoEvaluator struct {\n\tprepQuery        *rego.PreparedEvalQuery\n\taggrQuery        *rego.PreparedEvalQuery\n\tisPackageDefined bool\n}\n\nfunc (regoEvaluator *regoEvaluator) IsAggregationSupported() bool {\n\treturn regoEvaluator.aggrQuery != nil\n}\n\nfunc (regoEvaluator *regoEvaluator) Eval(in map[string]interface{}, serverUrl string) (map[string]string, error) {\n\tctx := context.Background()\n\trs, err := regoEvaluator.prepQuery.Eval(ctx, rego.EvalInput(in))\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(rs) == 0 || len(rs[0].Expressions) == 0 {\n\t\treturn nil, errors.New(\"no results\") //TODO error definition\n\t}\n\n\tvar expr interface{}\n\tif regoEvaluator.isPackageDefined {\n\t\texpr = rs[0].Expressions[0].Value\n\t} else {\n\t\texpr = getFirstElement(rs[0].Expressions[0].Value.(map[string]interface{}), result_prop)\n\t\tif expr == nil {\n\t\t\treturn nil, errors.New(\"invalid rego template structure\")\n\t\t}\n\t}\n\n\tdata := expr.(map[string]interface{})\n\n\ttitle, err := asStringOrJson(data, title_prop)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdescription, err := asStringOrJson(data, result_prop)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tshortMessageUrl, ok := data[url_prop].(string)\n\tif !ok {\n\t\tshortMessageUrl = \"\"\n\t}\n\n\t// variables for servicenow\n\t// for other templates must be empty\n\tdate := getStringFromData(data, dateProp)\n\tseverity := getStringFromData(data, severityProp)\n\tcategory := getStringFromData(data, categoryProp)\n\tsubcategory := getStringFromData(data, subcategoryProp)\n\tassignedTo := getStringFromData(data, assignedToProp)\n\tassignedGroup := getStringFromData(data, assignedGroupProp)\n\tsummary := getStringFromData(data, summaryProp)\n\n\treturn map[string]string{\n\t\t\"title\":         title,\n\t\t\"description\":   description,\n\t\t\"url\":           shortMessageUrl,\n\t\t\"date\":          date,\n\t\t\"severity\":      severity,\n\t\t\"summary\":       summary,\n\t\t\"category\":      category,\n\t\t\"subcategory\":   subcategory,\n\t\t\"assignedTo\":    assignedTo,\n\t\t\"assignedGroup\": assignedGroup,\n\t}, nil\n\n}\nfunc getStringFromData(data map[string]interface{}, prop string) string {\n\tvalue := \"\"\n\tv, ok := data[prop]\n\tif ok {\n\t\tswitch v.(type) {\n\t\tcase string:\n\t\t\tvalue = v.(string)\n\t\tcase json.Number:\n\t\t\tvalue = v.(json.Number).String()\n\t\t}\n\n\t}\n\treturn value\n}\n\nfunc getFirstElement(context map[string]interface{}, key string) interface{} {\n\tfor _, v := range context {\n\t\tlog.Printf(\"checking: %s ...\\n\", key)\n\t\tchildCtx, ok := v.(map[string]interface{})\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\tif childCtx[key] != nil {\n\t\t\treturn v\n\t\t} else {\n\t\t\tfound := getFirstElement(childCtx, key)\n\t\t\tif found != nil {\n\t\t\t\treturn found\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc asStringOrJson(data map[string]interface{}, prop string) (string, error) {\n\texpr, ok := data[prop]\n\tif !ok {\n\t\treturn \"\", errors.New(fmt.Sprintf(\"property %s is not found\", prop))\n\t}\n\tswitch v := expr.(type) { // TODO: Use json.Valid() instead\n\tcase string:\n\t\treturn v, nil\n\tdefault:\n\t\tval, err := json.Marshal(expr)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tvar out bytes.Buffer\n\t\tif err = json.Compact(&out, val); err != nil { // Remove extra '\\n' et al.\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn out.String(), nil\n\t}\n}\nfunc (regoEvaluator *regoEvaluator) BuildAggregatedContent(scans []map[string]string) (map[string]string, error) {\n\taggregatedJson := make([]map[string]interface{}, len(scans), len(scans))\n\n\tfor _, scan := range scans {\n\t\tdesc := scan[\"description\"]\n\t\tvar in []map[string]interface{}\n\n\t\titem := make(map[string]interface{})\n\n\t\tif err := json.Unmarshal([]byte(desc), &in); err != nil {\n\t\t\titem[\"description\"] = desc //description is not json, so it's passed as string\n\t\t} else {\n\t\t\titem[\"description\"] = in\n\t\t}\n\n\t\titem[\"title\"] = scan[\"title\"]\n\n\t\titem[url_prop] = scan[url_prop]\n\n\t\t// ServiceNow\n\t\titem[\"date\"] = scan[\"date\"]\n\t\titem[\"severity\"] = scan[\"severity\"]\n\t\titem[\"summary\"] = scan[\"summary\"]\n\t\titem[\"category\"] = scan[\"category\"]\n\t\titem[\"subcategory\"] = scan[\"subcategory\"]\n\t\titem[\"assignedTo\"] = scan[\"assignedTo\"]\n\t\titem[\"assignedGroup\"] = scan[\"assignedGroup\"]\n\n\t\taggregatedJson = append(aggregatedJson, item)\n\t}\n\n\tctx := context.Background()\n\trs, err := regoEvaluator.aggrQuery.Eval(ctx, rego.EvalInput(aggregatedJson))\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif len(rs) == 0 || len(rs[0].Expressions) == 0 {\n\t\treturn nil, errors.New(\"no results\") //TODO error definition\n\t}\n\n\texpr := rs[0].Expressions[0].Value\n\n\tdata := expr.(map[string]interface{})\n\n\ttitle, err := asStringOrJson(data, title_prop)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdescription, err := asStringOrJson(data, result_prop)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tshortMessageUrl, ok := data[url_prop].(string)\n\tif !ok {\n\t\tshortMessageUrl = \"\"\n\t}\n\n\t// variables for servicenow\n\t// for other templates must be empty\n\tdate := getStringFromData(data, dateProp)\n\tseverity := getStringFromData(data, severityProp)\n\tcategory := getStringFromData(data, categoryProp)\n\tsubcategory := getStringFromData(data, subcategoryProp)\n\tassignedTo := getStringFromData(data, assignedToProp)\n\tassignedGroup := getStringFromData(data, assignedGroupProp)\n\tsummary := getStringFromData(data, summaryProp)\n\n\treturn map[string]string{\n\t\t\"title\":         title,\n\t\t\"description\":   description,\n\t\t\"url\":           shortMessageUrl,\n\t\t\"date\":          date,\n\t\t\"severity\":      severity,\n\t\t\"summary\":       summary,\n\t\t\"category\":      category,\n\t\t\"subcategory\":   subcategory,\n\t\t\"assignedTo\":    assignedTo,\n\t\t\"assignedGroup\": assignedGroup,\n\t}, nil\n}\n\nfunc BuildBundledRegoEvaluator(rego_package string) (data.Inpteval, error) {\n\tr, err := buildBundledRegoForPackage(rego_package)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\taggrQuery, err := buildAggregatedRego(r)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &regoEvaluator{\n\t\tprepQuery:        r,\n\t\tisPackageDefined: true,\n\t\taggrQuery:        aggrQuery,\n\t}, nil\n}\nfunc buildBundledRegoForPackage(rego_package string) (*rego.PreparedEvalQuery, error) {\n\tctx := context.Background()\n\tquery := fmt.Sprintf(\"data.%s\", rego_package)\n\n\tr, err := rego.New(\n\t\trego.Query(query),\n\t\tjsonFmtFunc(),\n\t\trego.Load(buildinRegoTemplates, filterRegoTemplateFiles),\n\t).PrepareForEval(ctx)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &r, nil\n}\n\n// there is case when k8s creates `lost+found` file without access (bad permission) in template folder\n// skip this file to avoid error\nfunc filterRegoTemplateFiles(_ string, info fs.FileInfo, _ int) bool {\n\tif info.Name() == \"lost+found\" {\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc buildAggregatedRego(query *rego.PreparedEvalQuery) (*rego.PreparedEvalQuery, error) {\n\tctx := context.Background()\n\n\t//execute query with empty input and check if aggregation package is defined\n\trs, err := query.Eval(ctx, rego.EvalInput(make(map[string]interface{})))\n\n\tif len(rs) == 0 || len(rs[0].Expressions) == 0 {\n\t\treturn nil, errors.New(\"no results\") //TODO error definition\n\t}\n\n\texpr := rs[0].Expressions[0].Value.(map[string]interface{})\n\n\taggregation_pkg_val := expr[aggregation_pkg_prop]\n\n\tvar aggrQuery *rego.PreparedEvalQuery\n\n\tif aggregation_pkg_val != nil {\n\t\taggregation_pkg := aggregation_pkg_val.(string)\n\t\taggrQuery, err = buildBundledRegoForPackage(aggregation_pkg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\t//it's ok skip aggregation package - no aggregation features will be available\n\t\tlog.Printf(\"No aggregation package configured!!!\")\n\t}\n\treturn aggrQuery, nil\n}\n\nfunc BuildExternalRegoEvaluator(filename string, body string) (data.Inpteval, error) {\n\tctx := context.Background()\n\n\tr, err := rego.New(\n\t\trego.Query(\"data\"),\n\t\tjsonFmtFunc(),\n\t\trego.Load(commonRegoTemplates, filterRegoTemplateFiles), //only common modules\n\t\trego.Module(filename, body),\n\t).PrepareForEval(ctx)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\taggrQuery, err := buildAggregatedRego(&r)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &regoEvaluator{\n\t\tprepQuery:        &r,\n\t\tisPackageDefined: false,\n\t\taggrQuery:        aggrQuery,\n\t}, nil\n}\n"
  },
  {
    "path": "regoservice/eval_test.go",
    "content": "package regoservice\n\nimport (\n\t\"encoding/json\"\n\t\"flag\"\n\t\"github.com/stretchr/testify/require\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nvar update = flag.Bool(\"update\", false, \"update golden files\")\n\nfunc TestEval(t *testing.T) {\n\ttests := []struct {\n\t\tregoRule                *string\n\t\ttemplateFile            string\n\t\tcaseDesc                string\n\t\tinputFile               string\n\t\tregoPackage             string\n\t\texpectedValues          map[string]string // Description saves in golden file\n\t\texpectedDescriptionFile string\n\t\tshouldEvalFail          bool\n\t\tshouldPrepareFail       bool\n\t\tskipBuildin             bool\n\t\tskipExternal            bool\n\t}{\n\t\t/* cases for basic functionality */\n\t\t{\n\t\t\tcaseDesc:                \"simple case producing html output\",\n\t\t\tinputFile:               \"testdata/inputs/simple-input.json\",\n\t\t\ttemplateFile:            \"testdata/templates/html.rego\",\n\t\t\tregoPackage:             \"rego1.html\",\n\t\t\texpectedDescriptionFile: \"testdata/goldens/html.golden\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"Audit event received\",\n\t\t\t\t\"url\":   \"Audit-registry-received/Audit-image-received\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tcaseDesc:                \"Multilevel package\",\n\t\t\tinputFile:               \"testdata/inputs/simple-input.json\",\n\t\t\ttemplateFile:            \"testdata/templates/html-with-complex-pkg.rego\",\n\t\t\tregoPackage:             \"rego2.html\",\n\t\t\texpectedDescriptionFile: \"testdata/goldens/html-with-complex-pkg.golden\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"Audit event received\",\n\t\t\t\t\"url\":   \"Audit-registry-received/Audit-image-received\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tcaseDesc:                \"producing json output\",\n\t\t\tinputFile:               \"testdata/inputs/simple-input.json\",\n\t\t\ttemplateFile:            \"testdata/templates/json.rego\",\n\t\t\tregoPackage:             \"rego1.json\",\n\t\t\texpectedDescriptionFile: \"testdata/goldens/json.golden\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"Audit event received\",\n\t\t\t\t\"url\":   \"Audit-registry-received/Audit-image-received\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tcaseDesc:                \"producing json output without url\",\n\t\t\tinputFile:               \"testdata/inputs/simple-input.json\",\n\t\t\ttemplateFile:            \"testdata/templates/json-without-url.rego\",\n\t\t\tregoPackage:             \"rego1.json.without.url\",\n\t\t\texpectedDescriptionFile: \"testdata/goldens/json-without-url.golden\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"Audit event received\",\n\t\t\t\t\"url\":   \"\",\n\t\t\t},\n\t\t},\n\t\t/* cases for templates from `rego-templates` directory */\n\t\t{\n\t\t\tcaseDesc:     \"raw-message-html.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/simple-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/raw-message-html.rego\",\n\t\t\tregoPackage:  \"postee.rawmessage.html\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"Raw Message Received\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/raw-message-html.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"raw-message-json.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/simple-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/raw-message-json.rego\",\n\t\t\tregoPackage:  \"postee.rawmessage.json\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"-\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/raw-message-json.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"trivy-jira.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/trivy-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/trivy-jira.rego\",\n\t\t\tregoPackage:  \"postee.trivy.jira\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"pom.xml vulnerability scan report\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/trivy-jira.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"trivy-vulns-slack.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/trivy-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/trivy-vulns-slack.rego\",\n\t\t\tregoPackage:  \"postee.vuls.trivy.slack\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"Vulnerability scan report\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/trivy-vulns-slack.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"vuls-html.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/aqua-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/vuls-html.rego\",\n\t\t\tregoPackage:  \"postee.vuls.html\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"all-in-one:3.5.19223 vulnerability scan report\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/vuls-html.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"vuls-html.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/aqua-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/vuls-html.rego\",\n\t\t\tregoPackage:  \"postee.vuls.html\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"all-in-one:3.5.19223 vulnerability scan report\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/vuls-html.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"vuls-slack.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/aqua-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/vuls-slack.rego\",\n\t\t\tregoPackage:  \"postee.vuls.slack\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"all-in-one:3.5.19223 vulnerability scan report\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/vuls-slack.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"vuls-cyclonedx.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/aqua-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/vuls-cyclonedx.rego\",\n\t\t\tregoPackage:  \"postee.vuls.cyclondx\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"all-in-one:3.5.19223\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/vuls-cyclonedx.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"servicenow.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/aqua-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/servicenow.rego\",\n\t\t\tregoPackage:  \"postee.servicenow\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\":         \"Aqua security | image | all-in-one:3.5.19223 | Scan report\",\n\t\t\t\t\"category\":      \"Security Image Scan results\",\n\t\t\t\t\"subcategory\":   \"Security incident\",\n\t\t\t\t\"date\":          \"1624544066\",\n\t\t\t\t\"severity\":      \"1\",\n\t\t\t\t\"summary\":       \"Name: all-in-one:3.5.19223\\nRegistry: Aqua\\nMalware found: Yes\\nSensitive data found: Yes\\n\\nvulnerabilities:\\n*   critical: 1,\\n*   high: 1,\\n*   medium: 1,\\n*   low: 1,\\n*   negligible: 1\\n\\n\",\n\t\t\t\t\"assignedTo\":    \"owner\",\n\t\t\t\t\"assignedGroup\": \"group\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/servicenow.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"servicenow-incident.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/aqua-incident-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/servicenow-incident.rego\",\n\t\t\tregoPackage:  \"postee.servicenow.incident\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\":    \"test\",\n\t\t\t\t\"category\": \"Security incident\",\n\t\t\t\t\"severity\": \"3\",\n\t\t\t\t\"summary\":  \"Category: Test\\nSeverity: 3\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/servicenow-incident.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"servicenow-insight.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/aqua-insight-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/servicenow-insight.rego\",\n\t\t\tregoPackage:  \"postee.servicenow.insight\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\":    \"Workloads or images containing login data\",\n\t\t\t\t\"category\": \"Security insight\",\n\t\t\t\t\"severity\": \"2\",\n\t\t\t\t\"summary\":  \"Insight ID: aqua-3006\\nDescription: Workloads or images containing login data\\nImpact: Attackers with access to this workload or image might be able to use the login data to gain initial access to other resources\\nSeverity: medium\\nFound Date: 2022-08-25T09:02:28.991Z\\nLast Scan: 2022-08-25T08:59:42.314673Z\\nURL: \",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/servicenow-insight.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"trivy-operator-jira.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/trivy-operator-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/trivy-operator-jira.rego\",\n\t\t\tregoPackage:  \"postee.trivyoperator.jira\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"Vulnerability issue with image library/nginx:1.16 in namespace default\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/trivy-operator-jira.golden\",\n\t\t},\n\t\t{\n\t\t\tcaseDesc:     \"trivy-operator-slack.rego template\",\n\t\t\tinputFile:    \"testdata/inputs/trivy-operator-input.json\",\n\t\t\ttemplateFile: \"../rego-templates/trivy-operator-slack.rego\",\n\t\t\tregoPackage:  \"postee.trivyoperator.slack\",\n\t\t\texpectedValues: map[string]string{\n\t\t\t\t\"title\": \"Vulnerability scan report library/nginx:1.16\",\n\t\t\t},\n\t\t\texpectedDescriptionFile: \"testdata/goldens/trivy-operator-slack.golden\",\n\t\t},\n\t\t/* cases which should fail are below*/\n\t\t{\n\t\t\tcaseDesc:          \"Rego with wrong package specified\",\n\t\t\tinputFile:         \"testdata/inputs/simple-input.json\",\n\t\t\ttemplateFile:      \"testdata/templates/without-result.rego\",\n\t\t\tregoPackage:       \"rego3\",\n\t\t\texpectedValues:    map[string]string{},\n\t\t\tshouldPrepareFail: true,\n\t\t\tskipExternal:      true,\n\t\t},\n\t\t{\n\t\t\tcaseDesc:       \"Rego without any expression\",\n\t\t\tinputFile:      \"testdata/inputs/simple-input.json\",\n\t\t\ttemplateFile:   \"testdata/templates/without-any-expression.rego\",\n\t\t\tregoPackage:    \"rego1.without.any.expression\",\n\t\t\tshouldEvalFail: true,\n\t\t},\n\t\t{\n\t\t\tcaseDesc:       \"Invalid Rego\",\n\t\t\tinputFile:      \"testdata/inputs/simple-input.json\",\n\t\t\ttemplateFile:   \"testdata/templates/invalid.rego\",\n\t\t\tregoPackage:    \"rego1.invalid\",\n\t\t\texpectedValues: map[string]string{},\n\t\t\tshouldEvalFail: true,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.caseDesc, func(t *testing.T) {\n\t\t\tif !test.skipBuildin {\n\t\t\t\tevaluateBuildinRego(t, test.inputFile, test.templateFile, test.expectedDescriptionFile, test.regoPackage, test.expectedValues, test.shouldEvalFail, test.shouldPrepareFail)\n\t\t\t}\n\n\t\t\tif !test.skipExternal {\n\t\t\t\tevaluateExternalRego(t, test.inputFile, test.templateFile, test.expectedDescriptionFile, test.expectedValues, test.shouldEvalFail, test.shouldPrepareFail)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc evaluateBuildinRego(t *testing.T, inputFile, templateFile, descriptionGoldenFile, regoPackage string, expectedValues map[string]string, shouldEvalFail bool, shouldPrepareFail bool) {\n\tbuildinRegoTemplatesSaved := buildinRegoTemplates\n\tbuildinRegoTemplates = []string{filepath.Dir(templateFile)}\n\tdefer func() {\n\t\tbuildinRegoTemplates = buildinRegoTemplatesSaved\n\t}()\n\n\tdemo, err := BuildBundledRegoEvaluator(regoPackage)\n\tif shouldPrepareFail {\n\t\trequire.Error(t, err, \"test case should fail on prepare\")\n\t\treturn\n\t}\n\trequire.NoError(t, err)\n\n\tf, err := os.Open(inputFile)\n\trequire.NoError(t, err)\n\tdefer f.Close()\n\n\tin := make(map[string]interface{})\n\terr = json.NewDecoder(f).Decode(&in)\n\trequire.NoError(t, err)\n\n\tr, err := demo.Eval(in, \"\")\n\tif shouldEvalFail {\n\t\trequire.Error(t, err, \"test case should fail on eval\")\n\t\treturn\n\t}\n\trequire.NoError(t, err)\n\n\t// write description in file\n\tdescriptionFile := filepath.Join(t.TempDir(), \"description.txt\")\n\tif *update {\n\t\tdescriptionFile = descriptionGoldenFile\n\t}\n\n\terr = os.WriteFile(descriptionFile, []byte(r[\"description\"]), 0644)\n\trequire.NoError(t, err)\n\n\tcompareDescriptions(t, descriptionGoldenFile, descriptionFile)\n\n\tfor key, expected := range expectedValues {\n\t\twant := r[key]\n\t\trequire.EqualValues(t, expected, want)\n\t}\n}\nfunc evaluateExternalRego(t *testing.T, inputFile, templateFile, descriptionGoldenFile string, expectedValues map[string]string, shouldEvalFail bool, shouldPrepareFail bool) {\n\tcommonRegoTemplatesSaved := commonRegoTemplates\n\tcommonRegoDir := filepath.Join(filepath.Dir(templateFile), \"common\", \"common.rego\")\n\tcommonRegoTemplates = []string{commonRegoDir}\n\tdefer func() {\n\t\tcommonRegoTemplates = commonRegoTemplatesSaved\n\t}()\n\n\tb, err := os.ReadFile(templateFile)\n\trequire.NoError(t, err)\n\n\tdemo, err := BuildExternalRegoEvaluator(templateFile, string(b))\n\tif shouldPrepareFail {\n\t\trequire.Error(t, err, \"test case should fail on prepare\")\n\t\treturn\n\t}\n\trequire.NoError(t, err)\n\n\tf, err := os.Open(inputFile)\n\trequire.NoError(t, err)\n\tdefer f.Close()\n\n\tin := make(map[string]interface{})\n\terr = json.NewDecoder(f).Decode(&in)\n\trequire.NoError(t, err)\n\n\tr, err := demo.Eval(in, \"\")\n\tif shouldEvalFail {\n\t\trequire.Error(t, err, \"test case should fail on eval\")\n\t\treturn\n\t}\n\trequire.NoError(t, err)\n\n\t// write description in file\n\tdescriptionFile := filepath.Join(t.TempDir(), \"description.txt\")\n\tif *update {\n\t\tdescriptionFile = descriptionGoldenFile\n\t}\n\n\terr = os.WriteFile(descriptionFile, []byte(r[\"description\"]), 0644)\n\trequire.NoError(t, err)\n\n\tcompareDescriptions(t, descriptionGoldenFile, descriptionFile)\n\n\tfor key, expected := range expectedValues {\n\t\twant := r[key]\n\t\trequire.EqualValues(t, expected, want)\n\t}\n}\n\nfunc compareDescriptions(t *testing.T, expectedFile, gotFile string) {\n\texpected, err := os.ReadFile(expectedFile)\n\trequire.NoError(t, err)\n\tgot, err := os.ReadFile(gotFile)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, string(expected), string(got))\n}\n\nfunc TestBuildBundledRegoForPackage(t *testing.T) {\n\tregoRule := `\npackage rego1\ntitle:=\"Audit event received\"\nresult:=sprintf(\"Audit event received from %s\", [input.user])\t\nurl:=\"Audit-registry-received/Audit-image-received\"\n`\n\ttests := []struct {\n\t\tname      string\n\t\tfileName  string\n\t\tperm      fs.FileMode\n\t\twantRules bool\n\t\twantErr   string\n\t}{\n\t\t{\n\t\t\tname:      \"happy path\",\n\t\t\tfileName:  \"rego1.rego\",\n\t\t\tperm:      0644,\n\t\t\twantRules: true,\n\t\t},\n\t\t{\n\t\t\tname:     \"bad permission\",\n\t\t\tfileName: \"rego1.rego\",\n\t\t\tperm:     0000,\n\t\t\twantErr:  \"permission denied\",\n\t\t},\n\t\t{\n\t\t\tname:     \"lost+found\",\n\t\t\tfileName: \"lost+found\",\n\t\t\tperm:     0644,\n\t\t},\n\t\t{\n\t\t\tname:     \"lost+found with bad permission\",\n\t\t\tfileName: \"lost+found\",\n\t\t\tperm:     0000,\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tregoFilePath := filepath.Join(t.TempDir(), tt.fileName)\n\t\t\terr := os.WriteFile(regoFilePath, []byte(regoRule), tt.perm)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsavedBuildinRegoTemplates := buildinRegoTemplates\n\t\t\tbuildinRegoTemplates = []string{regoFilePath}\n\t\t\tdefer func() {\n\t\t\t\tbuildinRegoTemplates = savedBuildinRegoTemplates\n\t\t\t}()\n\n\t\t\tr, err := buildBundledRegoForPackage(\"rego1\")\n\t\t\tif tt.wantErr != \"\" {\n\t\t\t\trequire.ErrorContains(t, err, tt.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif tt.wantRules {\n\t\t\t\trequire.NotEmpty(t, r.Modules())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Empty(t, r.Modules())\n\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "regoservice/jsonformat.go",
    "content": "package regoservice\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\n\t\"github.com/open-policy-agent/opa/ast\"\n\t\"github.com/open-policy-agent/opa/rego\"\n\t\"github.com/open-policy-agent/opa/types\"\n)\n\nfunc jsonFmtFunc() func(r *rego.Rego) {\n\treturn rego.Function1(\n\t\t&rego.Function{\n\t\t\tName: \"jsonformat\",\n\t\t\tDecl: types.NewFunction(types.Args(&types.Object{}), types.S),\n\t\t},\n\n\t\tfunc(_ rego.BuiltinContext, a *ast.Term) (*ast.Term, error) {\n\t\t\tobj := make(map[string]interface{})\n\t\t\terr := ast.As(a.Value, &obj)\n\t\t\tif err != nil {\n\t\t\t\t//Rego doesn't show errors\n\t\t\t\tlog.Printf(\"Can't convert OPA object: %v\\n\", err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tb, err := json.MarshalIndent(obj, \"\", \" \")\n\t\t\tif err != nil {\n\t\t\t\t//Rego doesn't show errors\n\t\t\t\tlog.Printf(\"Error while json format: %v\\n\", err)\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn ast.StringTerm(string(b)), nil\n\t\t})\n}\n"
  },
  {
    "path": "regoservice/regocheck.go",
    "content": "package regoservice\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/open-policy-agent/opa/rego\"\n)\n\nconst (\n\tmodule = `package postee\n\ndefault allow = false\n\nallow {\n%s\n}\n`\n\tdefaultPathToRegoFilters = \"./rego-filters\"\n)\n\nvar pathToRegoFilters = \"\"\n\nfunc getFilesWithPathToRegoFilters(files []string) []string {\n\tif pathToRegoFilters == \"\" {\n\t\tif os.Getenv(\"REGO_FILTERS_PATH\") != \"\" {\n\t\t\tpathToRegoFilters = os.Getenv(\"REGO_FILTERS_PATH\")\n\t\t} else {\n\t\t\tpathToRegoFilters = defaultPathToRegoFilters\n\t\t}\n\t}\n\tfilesWithPath := make([]string, len(files))\n\tcopy(filesWithPath, files)\n\tfor i, file := range filesWithPath {\n\t\tif !strings.HasPrefix(file, pathToRegoFilters) {\n\t\t\tfilesWithPath[i] = filepath.Join(pathToRegoFilters, file)\n\t\t}\n\t}\n\treturn filesWithPath\n}\n\nfunc buildRegoLoader(files []string, rule string) func(r *rego.Rego) {\n\tif IsUsedRegoFiles(files) {\n\t\tfilesWithPath := getFilesWithPathToRegoFilters(files)\n\t\treturn rego.Load(filesWithPath, nil)\n\t}\n\n\treturn rego.Module(\"postee.rego\", fmt.Sprintf(module, rule))\n}\nfunc IsUsedRegoFiles(files []string) bool {\n\treturn len(files) != 0 && files[0] != \"\"\n}\nfunc DoesMatchRegoCriteria(input interface{}, files []string, rule string) (bool, error) {\n\tif !IsUsedRegoFiles(files) && rule == \"\" {\n\t\treturn true, nil\n\t}\n\n\tctx := context.Background()\n\n\tr := rego.New(\n\t\trego.Query(\"x = data.postee.allow\"),\n\t\tbuildRegoLoader(files, rule),\n\t)\n\n\tquery, err := r.PrepareForEval(ctx)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\trs, err := query.Eval(ctx, rego.EvalInput(input))\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif len(rs) > 0 {\n\t\tswitch rs[0].Bindings[\"x\"].(type) {\n\t\tcase bool:\n\t\t\treturn rs[0].Bindings[\"x\"].(bool), nil\n\t\t}\n\t}\n\treturn false, nil\n}\n"
  },
  {
    "path": "regoservice/regocheck_test.go",
    "content": "package regoservice\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestOpaRego(t *testing.T) {\n\trego := `contains(input.image, \"alpine\")`\n\tincorrectRego := `default input = false`\n\temptyRego := \"\"\n\n\tcorrectInputFiles := []string{\"../correctInputFiles.rego\"}\n\tincorrectInputFiles := []string{\"../incorrectInputFiles.rego\"}\n\temptyinputFiles := []string{}\n\tcorrectFile, err := os.Create(\"correctInputFiles.rego\")\n\tif err != nil {\n\t\tt.Errorf(\"error create file: %v\", err)\n\t}\n\t_, err = correctFile.WriteString(fmt.Sprintf(module, rego))\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer os.Remove(\"correctInputFiles.rego\")\n\tdefer correctFile.Close()\n\tincorrectFile, err := os.Create(\"incorrectInputFiles.rego\")\n\tif err != nil {\n\t\tt.Errorf(\"error create file: %v\", err)\n\t}\n\t_, err = incorrectFile.WriteString(fmt.Sprintf(module, incorrectRego))\n\tif err != nil {\n\t\tt.Errorf(\"error create file: %v\", err)\n\t}\n\tdefer os.Remove(\"incorrectInputFiles.rego\")\n\tdefer incorrectFile.Close()\n\n\tscanResult := `{\"image\":\"alpine:26\"}`\n\tscanNoJson := \"simple text\"\n\tscanWithoutResult := `{\"image\":\"1science:latest\"}`\n\n\ttests := []struct {\n\t\trules              string\n\t\tinputFiles         []string\n\t\tscan               string\n\t\tresult             bool\n\t\tshouldTriggerError bool\n\t}{\n\t\t{rego, emptyinputFiles, scanResult, true, false},\n\t\t{rego, emptyinputFiles, scanNoJson, false, true},\n\t\t{rego, emptyinputFiles, scanWithoutResult, false, false},\n\t\t{emptyRego, correctInputFiles, scanResult, true, false},\n\t\t{emptyRego, incorrectInputFiles, scanNoJson, false, true},\n\t\t{emptyRego, emptyinputFiles, scanWithoutResult, true, false},\n\t\t{incorrectRego, emptyinputFiles, scanResult, false, true},\n\t\t{emptyRego, emptyinputFiles, scanResult, true, false},\n\t}\n\n\tfor _, test := range tests {\n\t\tintr := map[string]interface{}{}\n\t\tif err := json.Unmarshal([]byte(test.scan), &intr); err != nil && !test.shouldTriggerError {\n\t\t\tt.Errorf(\"json.Unmarshal(%q) error: %v\", test.scan, err)\n\t\t\tcontinue\n\t\t}\n\n\t\tgot, err := DoesMatchRegoCriteria(intr, test.inputFiles, test.rules)\n\t\tif err != nil && !test.shouldTriggerError {\n\t\t\tt.Errorf(\"received an unexpected error: %v\", err)\n\t\t\tcontinue\n\t\t}\n\t\tif got != test.result {\n\t\t\tt.Errorf(\"DoesMatchRegoCriteria(%q, %q, %q) == %t, wanted %t\", test.scan, test.inputFiles, test.rules, got, test.result)\n\t\t}\n\t}\n}\n\nfunc TestGetFilesWithPathToRegoFilters(t *testing.T) {\n\toldEnv := os.Getenv(\"REGO_FILTERS_PATH\")\n\tdefer os.Setenv(\"REGO_FILTERS_PATH\", oldEnv)\n\toldPathToRegoFilters := pathToRegoFilters\n\n\ttests := []struct {\n\t\tfiles         []string\n\t\tenv           string\n\t\texpectedfiles []string\n\t}{\n\t\t{[]string{\"policy.rego\", \"ignore.rego\"}, \"\", []string{\"rego-filters/policy.rego\", \"rego-filters/ignore.rego\"}},\n\t\t{[]string{\"policy.rego\", \"ignore.rego\"}, \"filters\", []string{\"filters/policy.rego\", \"filters/ignore.rego\"}},\n\t\t{[]string{\"policy.rego\", \"ignore.rego\"}, \"filters/regofiles\", []string{\"filters/regofiles/policy.rego\", \"filters/regofiles/ignore.rego\"}},\n\t\t{[]string{\"policy.rego\", \"ignore.rego\"}, \"/filters/regofiles\", []string{\"/filters/regofiles/policy.rego\", \"/filters/regofiles/ignore.rego\"}},\n\t\t{[]string{}, \"./rego\", []string{}},\n\t}\n\n\tfor _, test := range tests {\n\t\tpathToRegoFilters = \"\"\n\t\tos.Setenv(\"REGO_FILTERS_PATH\", test.env)\n\t\tfmt.Println(pathToRegoFilters)\n\t\tfilesWithPath := getFilesWithPathToRegoFilters(test.files)\n\n\t\tfor i := range test.expectedfiles {\n\t\t\tif test.expectedfiles[i] != filesWithPath[i] {\n\t\t\t\tt.Errorf(\"Error for env: %s\\n expected file: %s, got: %s\", test.env, test.expectedfiles[i], filesWithPath[i])\n\t\t\t}\n\t\t}\n\t}\n\n\tpathToRegoFilters = oldPathToRegoFilters\n}\n"
  },
  {
    "path": "regoservice/testdata/goldens/html-with-complex-pkg.golden",
    "content": "Audit event received from demo"
  },
  {
    "path": "regoservice/testdata/goldens/html.golden",
    "content": "Audit event received from demo"
  },
  {
    "path": "regoservice/testdata/goldens/json-without-url.golden",
    "content": "{\"assignee\":\"demo\"}"
  },
  {
    "path": "regoservice/testdata/goldens/json.golden",
    "content": "{\"assignee\":\"demo\"}"
  },
  {
    "path": "regoservice/testdata/goldens/raw-message-html.golden",
    "content": "<pre><code>{\n \"user\": \"demo\"\n}</code></pre>"
  },
  {
    "path": "regoservice/testdata/goldens/raw-message-json.golden",
    "content": "{\n \"user\": \"demo\"\n}"
  },
  {
    "path": "regoservice/testdata/goldens/servicenow-incident.golden",
    "content": "\n<p><b>Name:</b> test</p>\n<p><b>Category:</b> Test</p>\n<p><b>Severity:</b> 3</p>\n<p><b>Data:</b> \n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TD style='padding: 5px;'>host</TD>\n<TD style='padding: 5px;'> ubuntu</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>rule</TD>\n<TD style='padding: 5px;'> Test-Default-Policy</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>level</TD>\n<TD style='padding: 5px;'> block</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>action</TD>\n<TD style='padding: 5px;'> test malware delete</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>hostid</TD>\n<TD style='padding: 5px;'> host</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>hostip</TD>\n<TD style='padding: 5px;'> 10.100.102.19</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>reason</TD>\n<TD style='padding: 5px;'> Malware detection</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>result</TD>\n<TD style='padding: 5px;'> 2</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>tactic</TD>\n<TD style='padding: 5px;'> Defense Evasion, Execution, Privilege Escalation, Initial Access</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>control</TD>\n<TD style='padding: 5px;'> Malware scanning Control</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>malware</TD>\n<TD style='padding: 5px;'> Eicar-Test-Signature</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>subtype</TD>\n<TD style='padding: 5px;'> malware protection</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>category</TD>\n<TD style='padding: 5px;'> malware</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>resource</TD>\n<TD style='padding: 5px;'> /home/usr/tmp/malware/eicar.test</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>severity</TD>\n<TD style='padding: 5px;'> 4</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>rule_type</TD>\n<TD style='padding: 5px;'> host.runtime.policy (Test)</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>technique</TD>\n<TD style='padding: 5px;'> Execution Guardrails, Exploit Public Facing Application, Client Execution, Privilege Escalation, Remote Services</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>k8s_cluster</TD>\n<TD style='padding: 5px;'> Cluster-Test</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>malware_type</TD>\n<TD style='padding: 5px;'> Virus</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>resource_digest</TD>\n<TD style='padding: 5px;'> 0000</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>malware_scan_type</TD>\n<TD style='padding: 5px;'> file</TD>\n\n</TR>\n</TABLE>\n</p>\n\n<p><b>Resourse policy name:</b> test</p>\n<p><b>Resourse policy application scopes:</b> [\"scope1\", \"scope2\"]</p>\n"
  },
  {
    "path": "regoservice/testdata/goldens/servicenow-insight.golden",
    "content": "\n<!-- Insight Details -->\n<h2> <i>Insight Details:</i> </h2>\n<p><b>Insight ID:</b> aqua-3006</p>\n<p><b>Description:</b> Workloads or images containing login data</p>\n<p><b>Impact:</b> Attackers with access to this workload or image might be able to use the login data to gain initial access to other resources</p>\n<p><b>Severity:</b> medium</p>\n<p><b>Found Date:</b> 2022-08-25T09:02:28.991Z</p>\n<p><b>Last Scan:</b> 2022-08-25T08:59:42.314673Z</p>\n<p><b>URL:</b> </p>\n<!-- TODO  -->\n<!-- Resourse Details -->\n<h2> <i>Resourse Details:</i> </h2>\n<p><b>Resourse ID:</b> 6131180</p>\n<p><b>Resourse Name:</b> shayyo/sensitive_data:latest</p>\n<p><b>ARN:</b> </p>\n<p><b>Extra Info:</b> {\"Image\": \"sensitive_data:latest\", \"Registry\": \"Docker Hub\", \"RegistryDomain\": \"docker.io\"}</p>\n<!-- Evidence -->\n<h2> <i>Evidence:</i> </h2>\n<!-- Vulnerabilities -->\n\n<!-- Sensitive data -->\n\n<p>Sensitive data</p>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>File Type</TH>\n<TH style='padding: 5px;'>File Path</TH>\n<TH style='padding: 5px;'>Image</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>RSA PRIVATE KEY</TD>\n<TD style='padding: 5px;'>/private-key.pem</TD>\n<TD style='padding: 5px;'>shayyo/sensitive_data:latest</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- Recommendation -->\n<h2> <i>Recommendation:</i> </h2>\nNo Recommendation\n<p><b>Resourse policy name:</b> insights_login data</p>\n<p><b>Resourse policy application scopes:</b> [\"scope1\", \"scope2\"]</p>\n"
  },
  {
    "path": "regoservice/testdata/goldens/servicenow.golden",
    "content": "\n<p><b>Name:</b> all-in-one:3.5.19223</p>\n<p><b>Registry:</b> Aqua</p>\n<p><b>Malware found:</b> Yes</p>\n<p><b>Sensitive data found:</b> Yes</p>\n<!-- Stats -->\n<h3> Vulnerability summary </h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TD style='padding: 5px;'>critical</TD>\n<TD style='padding: 5px;'><span style='color:#c00000'>1</span></TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>high</TD>\n<TD style='padding: 5px;'><span style='color:#e0443d'>1</span></TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>medium</TD>\n<TD style='padding: 5px;'><span style='color:#f79421'>1</span></TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>low</TD>\n<TD style='padding: 5px;'><span style='color:#e1c930'>1</span></TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>negligible</TD>\n<TD style='padding: 5px;'><span style='color:green'>1</span></TD>\n\n</TR>\n</TABLE>\n\n<!-- Assurance controls -->\n\n<h3>Assurance controls</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>#</TH>\n<TH style='padding: 5px;'>Control</TH>\n<TH style='padding: 5px;'>Policy Name</TH>\n<TH style='padding: 5px;'>Status</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>1</TD>\n<TD style='padding: 5px;'>malware</TD>\n<TD style='padding: 5px;'>Default</TD>\n<TD style='padding: 5px;'>PASS</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>2</TD>\n<TD style='padding: 5px;'>license</TD>\n<TD style='padding: 5px;'>Default</TD>\n<TD style='padding: 5px;'>PASS</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>3</TD>\n<TD style='padding: 5px;'>max_severity</TD>\n<TD style='padding: 5px;'>Default</TD>\n<TD style='padding: 5px;'>PASS</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- Critical severity vulnerabilities -->\n\n<h3>Critical severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2018-1000517</TD>\n<TD style='padding: 5px;'>busybox</TD>\n<TD style='padding: 5px;'>1.28.4-r3</TD>\n<TD style='padding: 5px;'>1.29.0</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- High severity vulnerabilities -->\n\n<h3>High severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2021-33560</TD>\n<TD style='padding: 5px;'>libgcrypt</TD>\n<TD style='padding: 5px;'>1.8.3-r0</TD>\n<TD style='padding: 5px;'>1.8.8</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- Medium severity vulnerabilities -->\n\n<h3>Medium severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2019-12904</TD>\n<TD style='padding: 5px;'>libgcrypt</TD>\n<TD style='padding: 5px;'>1.8.3-r0</TD>\n<TD style='padding: 5px;'>1.8.3-r1</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- Low severity vulnerabilities -->\n\n<h3>Low severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2018-20679</TD>\n<TD style='padding: 5px;'>busybox</TD>\n<TD style='padding: 5px;'>1.28.4-r3</TD>\n<TD style='padding: 5px;'>1.30.0</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- Negligible severity vulnerabilities -->\n\n<h3>Negligible severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2019-5747</TD>\n<TD style='padding: 5px;'>busybox</TD>\n<TD style='padding: 5px;'>1.28.4-r3</TD>\n<TD style='padding: 5px;'>none</TD>\n\n</TR>\n</TABLE>\n\n\n<p><b>Resourse policy name:</b> </p>\n<p><b>Resourse policy application scopes:</b> [\"group\"]</p>\n\n"
  },
  {
    "path": "regoservice/testdata/goldens/trivy-jira.golden",
    "content": "\nh1. Image name: pom.xml\n\nh4. Critical severity vulnerabilities\n\n\n| || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url  \n| | pom.xml | log4j: deserialization of untrusted data in SocketServer | CVE-2019-17571 | log4j:log4j | none | 1.2.17 | 2.0-alpha1 | https://avd.aquasec.com/nvd/cve-2019-17571  \n| | pom.xml | log4j: SQL injection in Log4j 1.x when application is configured to use JDBCAppender | CVE-2022-23305 | log4j:log4j | none | 1.2.17 | none | https://avd.aquasec.com/nvd/cve-2022-23305  \n\n\n\nh4. High severity vulnerabilities\n\n\n| || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url  \n| | pom.xml | Remote code execution in Log4j 1.x when application is configured to use JMSAppender | CVE-2021-4104 | log4j:log4j | none | 1.2.17 | none | https://avd.aquasec.com/nvd/cve-2021-4104  \n| | pom.xml | log4j: Unsafe deserialization flaw in Chainsaw log viewer | CVE-2022-23307 | log4j:log4j | none | 1.2.17 | none | https://avd.aquasec.com/nvd/cve-2022-23307  \n\n\n\nh4. Medium severity vulnerabilities\n\n\n| || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url  \n| | pom.xml | log4j: Remote code execution in Log4j 1.x when application is configured to use JMSSink | CVE-2022-23302 | log4j:log4j | none | 1.2.17 | none | https://avd.aquasec.com/nvd/cve-2022-23302  \n\n\n\nh4. Low severity vulnerabilities\n\n\n| || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url  \n| | pom.xml | log4j: improper validation of certificate with host mismatch in SMTP appender | CVE-2020-9488 | log4j:log4j | none | 1.2.17 | 2.12.3, 2.13.2 | https://avd.aquasec.com/nvd/cve-2020-9488  \n\n\n\nh4. Unknown severity vulnerabilities\n\n\n| || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url  \n| | pom.xml | DoS via hashmap logging | CVE-2023-26464 | log4j:log4j | none | 1.2.17 | 2.0 | https://avd.aquasec.com/nvd/cve-2023-26464  \n\n\n"
  },
  {
    "path": "regoservice/testdata/goldens/trivy-operator-jira.golden",
    "content": "\nh1. Image: library/nginx:1.16 in namespace default\n\nh4. Summary totals:\n|critical: {color:#c00000}2{color}|high: {color:#e0443d}0{color}|medium: {color:#f79421}0{color}|low: {color:#e1c930}0{color}|unknown: {color:#505f79}0{color}|\n\n\nh4. Critical severity vulnerabilities\n\n\n|| ID || Title || Resource || Installed version || Fixed version || Url  ||\n| CVE-2019-20367 |  | libbsd0 | 0.9.1-2 | 0.9.1-2+deb10u1 | https://avd.aquasec.com/nvd/cve-2019-20367  |\n\n\n\nh4. High severity vulnerabilities\n\n\n|| ID || Title || Resource || Installed version || Fixed version || Url  ||\n| CVE-2018-25009 | libwebp: out-of-bounds read in WebPMuxCreateInternal | libwebp6 | 0.6.1-2 |  | https://avd.aquasec.com/nvd/cve-2018-25009  |\n\n\n\nh4. Medium severity vulnerabilities\n\n\n|| ID || Title || Resource || Installed version || Fixed version || Url  ||\n| CVE-2018-25010 | libwebp: out-of-bounds read in WebPMuxCreateInternal | libwebp3 | 0.6.1-2 |  | https://avd.aquasec.com/nvd/cve-2018-25009  |\n\n\n\nh4. Low severity vulnerabilities\n\n\n|| ID || Title || Resource || Installed version || Fixed version || Url  ||\n| CVE-2018-25011 | libwebp: out-of-bounds read in WebPMuxCreateInternal | libwebp4 | 0.6.1-2 |  | https://avd.aquasec.com/nvd/cve-2018-25009  |\n\n\n\nh4. Unknown severity vulnerabilities\n\n\n|| ID || Title || Resource || Installed version || Fixed version || Url  ||\n| CVE-2018-25012 | libwebp: out-of-bounds read in WebPMuxCreateInternal | libwebp5 | 0.6.1-2 |  | https://avd.aquasec.com/nvd/cve-2018-25009  |\n\n\n"
  },
  {
    "path": "regoservice/testdata/goldens/trivy-operator-slack.golden",
    "content": "[{\"text\":{\"text\":\"Vulnerability issue with image:library/nginx:1.16 in namespace default\",\"type\":\"plain_text\"},\"type\":\"header\"},{\"type\":\"divider\"},{\"elements\":[{\"text\":\"*Summary totals:*\",\"type\":\"mrkdwn\"}],\"type\":\"context\"},{\"elements\":[{\"text\":\"Critical: *2*\",\"type\":\"mrkdwn\"},{\"text\":\"High: *0*\",\"type\":\"mrkdwn\"},{\"text\":\"Medium: *0*\",\"type\":\"mrkdwn\"},{\"text\":\"Low: *0*\",\"type\":\"mrkdwn\"},{\"text\":\"Unknown: *0*\",\"type\":\"mrkdwn\"}],\"type\":\"context\"},{\"type\":\"divider\"},{\"text\":{\"text\":\"*CRITICAL severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource / Version / Fixed version*\",\"type\":\"mrkdwn\"},{\"text\":\"\\u003chttps://avd.aquasec.com/nvd/cve-2019-20367|CVE-2019-20367\\u003e\",\"type\":\"mrkdwn\"},{\"text\":\"libbsd0 / 0.9.1-2 / 0.9.1-2+deb10u1\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"type\":\"divider\"},{\"text\":{\"text\":\"*HIGH severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource / Version / Fixed version*\",\"type\":\"mrkdwn\"},{\"text\":\"\\u003chttps://avd.aquasec.com/nvd/cve-2018-25009|CVE-2018-25009\\u003e\",\"type\":\"mrkdwn\"},{\"text\":\"libwebp6 / 0.6.1-2 / \",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"type\":\"divider\"},{\"text\":{\"text\":\"*MEDIUM severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource / Version / Fixed version*\",\"type\":\"mrkdwn\"},{\"text\":\"\\u003chttps://avd.aquasec.com/nvd/cve-2018-25009|CVE-2018-25010\\u003e\",\"type\":\"mrkdwn\"},{\"text\":\"libwebp3 / 0.6.1-2 / \",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"type\":\"divider\"},{\"text\":{\"text\":\"*LOW severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource / Version / Fixed version*\",\"type\":\"mrkdwn\"},{\"text\":\"\\u003chttps://avd.aquasec.com/nvd/cve-2018-25009|CVE-2018-25011\\u003e\",\"type\":\"mrkdwn\"},{\"text\":\"libwebp4 / 0.6.1-2 / \",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"type\":\"divider\"},{\"text\":{\"text\":\"*UNKNOWN severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource / Version / Fixed version*\",\"type\":\"mrkdwn\"},{\"text\":\"\\u003chttps://avd.aquasec.com/nvd/cve-2018-25009|CVE-2018-25012\\u003e\",\"type\":\"mrkdwn\"},{\"text\":\"libwebp5 / 0.6.1-2 / \",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"type\":\"divider\"}]"
  },
  {
    "path": "regoservice/testdata/goldens/trivy-vulns-slack.golden",
    "content": "[{\"text\":{\"text\":\"Artifact name: pom.xml\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"text\":{\"text\":\"Type: filesystem\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"text\":{\"text\":\"*Found vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"text\":{\"text\":\"*CRITICAL severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2019-17571\",\"type\":\"mrkdwn\"},{\"text\":\"log4j:log4j / 1.2.17 / 2.0-alpha1\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2022-23305\",\"type\":\"mrkdwn\"},{\"text\":\"log4j:log4j / 1.2.17 / none\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*HIGH severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2021-4104\",\"type\":\"mrkdwn\"},{\"text\":\"log4j:log4j / 1.2.17 / none\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2022-23307\",\"type\":\"mrkdwn\"},{\"text\":\"log4j:log4j / 1.2.17 / none\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*MEDIUM severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2022-23302\",\"type\":\"mrkdwn\"},{\"text\":\"log4j:log4j / 1.2.17 / none\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*LOW severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2020-9488\",\"type\":\"mrkdwn\"},{\"text\":\"log4j:log4j / 1.2.17 / 2.12.3, 2.13.2\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*UNKNOWN severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2023-26464\",\"type\":\"mrkdwn\"},{\"text\":\"log4j:log4j / 1.2.17 / 2.0\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*Found vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"Critical\",\"type\":\"mrkdwn\"},{\"text\":\"*2*\",\"type\":\"mrkdwn\"},{\"text\":\"High\",\"type\":\"mrkdwn\"},{\"text\":\"*2*\",\"type\":\"mrkdwn\"},{\"text\":\"Medium\",\"type\":\"mrkdwn\"},{\"text\":\"*1*\",\"type\":\"mrkdwn\"},{\"text\":\"Low\",\"type\":\"mrkdwn\"},{\"text\":\"*1*\",\"type\":\"mrkdwn\"},{\"text\":\"Unknown\",\"type\":\"mrkdwn\"},{\"text\":\"*1*\",\"type\":\"mrkdwn\"}],\"type\":\"section\"}]"
  },
  {
    "path": "regoservice/testdata/goldens/vuls-cyclonedx.golden",
    "content": "<?xml version=\"1.0\"?>\n<bom serialNumber=\"urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79\" version=\"1\"\n     xmlns=\"http://cyclonedx.org/schema/bom/1.1\"\n     xmlns:v=\"http://cyclonedx.org/schema/ext/vulnerability/1.0\">\n  <components>\n      <component type=\"application\">\n      <name>libgcrypt</name>\n      <version>1.8.3-r0</version>\n      <licenses>\n        <license>\n          <id>LGPL</id>\n        </license>\n      </licenses>\n      <v:vulnerabilities>\n        <v:vulnerability>\n          <v:id>CVE-2019-12904</v:id> \n          <v:source name=\"NVD\">\n            <v:url>https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-12904</v:url>\n          </v:source>\n          <v:ratings>\n            <v:rating>\n              <v:score>\n                <v:base>4.3</v:base>\n                <v:impact>4.3</v:impact>\n                <v:exploitability>4.3</v:exploitability>\n              </v:score>\n              <v:severity>medium</v:severity>\n              <v:method>CVSS V2</v:method>\n              <v:vector>AV:N/AC:M/Au:N/C:P/I:N/A:N</v:vector>\n            </v:rating>\n          </v:ratings>\n          <v:recommendations>\n            <v:recommendation>Upgrade package libgcrypt to version 1.8.3-r1 or above.</v:recommendation>\n          </v:recommendations>\n        </v:vulnerability>\n        <v:vulnerability>\n          <v:id>CVE-2021-33560</v:id> \n          <v:source name=\"NVD\">\n            <v:url>https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-33560</v:url>\n          </v:source>\n          <v:ratings>\n            <v:rating>\n              <v:score>\n                <v:base>5</v:base>\n                <v:impact>5</v:impact>\n                <v:exploitability>5</v:exploitability>\n              </v:score>\n              <v:severity>high</v:severity>\n              <v:method>CVSS V2</v:method>\n              <v:vector>AV:N/AC:L/Au:N/C:P/I:N/A:N</v:vector>\n            </v:rating>\n          </v:ratings>\n          <v:recommendations>\n            <v:recommendation>No solution available</v:recommendation>\n          </v:recommendations>\n        </v:vulnerability></v:vulnerabilities>\n    </component>    <component type=\"application\">\n      <name>busybox</name>\n      <version>1.28.4-r3</version>\n      <licenses>\n        <license>\n          <id>GPL2</id>\n        </license>\n      </licenses>\n      <v:vulnerabilities>\n        <v:vulnerability>\n          <v:id>CVE-2018-1000517</v:id> \n          <v:source name=\"NVD\">\n            <v:url>https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1000517</v:url>\n          </v:source>\n          <v:ratings>\n            <v:rating>\n              <v:score>\n                <v:base>7.5</v:base>\n                <v:impact>7.5</v:impact>\n                <v:exploitability>7.5</v:exploitability>\n              </v:score>\n              <v:severity>critical</v:severity>\n              <v:method>CVSS V2</v:method>\n              <v:vector>AV:N/AC:L/Au:N/C:P/I:P/A:P</v:vector>\n            </v:rating>\n          </v:ratings>\n          <v:recommendations>\n            <v:recommendation>No solution available</v:recommendation>\n          </v:recommendations>\n        </v:vulnerability>\n        <v:vulnerability>\n          <v:id>CVE-2018-20679</v:id> \n          <v:source name=\"NVD\">\n            <v:url>https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-20679</v:url>\n          </v:source>\n          <v:ratings>\n            <v:rating>\n              <v:score>\n                <v:base>5</v:base>\n                <v:impact>5</v:impact>\n                <v:exploitability>5</v:exploitability>\n              </v:score>\n              <v:severity>low</v:severity>\n              <v:method>CVSS V2</v:method>\n              <v:vector>AV:N/AC:L/Au:N/C:P/I:N/A:N</v:vector>\n            </v:rating>\n          </v:ratings>\n          <v:recommendations>\n            <v:recommendation>No solution available</v:recommendation>\n          </v:recommendations>\n        </v:vulnerability>\n        <v:vulnerability>\n          <v:id>CVE-2019-5747</v:id> \n          <v:source name=\"NVD\">\n            <v:url>https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-5747</v:url>\n          </v:source>\n          <v:ratings>\n            <v:rating>\n              <v:score>\n                <v:base>5</v:base>\n                <v:impact>5</v:impact>\n                <v:exploitability>5</v:exploitability>\n              </v:score>\n              <v:severity>negligible</v:severity>\n              <v:method>CVSS V2</v:method>\n              <v:vector>AV:N/AC:L/Au:N/C:P/I:N/A:N</v:vector>\n            </v:rating>\n          </v:ratings>\n          <v:recommendations>\n            <v:recommendation>No solution available</v:recommendation>\n          </v:recommendations>\n        </v:vulnerability></v:vulnerabilities>\n    </component>\n  </components>\n</bom>"
  },
  {
    "path": "regoservice/testdata/goldens/vuls-html.golden",
    "content": "\n<p>Image name: all-in-one:3.5.19223</p>\n<p>Registry: Aqua</p>\n<p>Image is compliant</p>\n<p>Malware found: Yes</p>\n<p>Sensitive data found: Yes</p>\n<!-- stats -->\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TD style='padding: 5px;'>critical</TD>\n<TD style='padding: 5px;'><span style='color:#c00000'>1</span></TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>high</TD>\n<TD style='padding: 5px;'><span style='color:#e0443d'>1</span></TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>medium</TD>\n<TD style='padding: 5px;'><span style='color:#f79421'>1</span></TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>low</TD>\n<TD style='padding: 5px;'><span style='color:#e1c930'>1</span></TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>negligible</TD>\n<TD style='padding: 5px;'><span style='color:green'>1</span></TD>\n\n</TR>\n</TABLE>\n\n<h2>Assurance controls</h2>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>#</TH>\n<TH style='padding: 5px;'>Control</TH>\n<TH style='padding: 5px;'>Policy Name</TH>\n<TH style='padding: 5px;'>Status</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>1</TD>\n<TD style='padding: 5px;'>malware</TD>\n<TD style='padding: 5px;'>Default</TD>\n<TD style='padding: 5px;'>PASS</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>2</TD>\n<TD style='padding: 5px;'>license</TD>\n<TD style='padding: 5px;'>Default</TD>\n<TD style='padding: 5px;'>PASS</TD>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>3</TD>\n<TD style='padding: 5px;'>max_severity</TD>\n<TD style='padding: 5px;'>Default</TD>\n<TD style='padding: 5px;'>PASS</TD>\n\n</TR>\n</TABLE>\n\n<!-- Critical severity vulnerabilities -->\n\n<h3>Critical severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2018-1000517</TD>\n<TD style='padding: 5px;'>busybox</TD>\n<TD style='padding: 5px;'>1.28.4-r3</TD>\n<TD style='padding: 5px;'>1.29.0</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- High severity vulnerabilities -->\n\n<h3>High severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2021-33560</TD>\n<TD style='padding: 5px;'>libgcrypt</TD>\n<TD style='padding: 5px;'>1.8.3-r0</TD>\n<TD style='padding: 5px;'>1.8.8</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- Medium severity vulnerabilities -->\n\n<h3>Medium severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2019-12904</TD>\n<TD style='padding: 5px;'>libgcrypt</TD>\n<TD style='padding: 5px;'>1.8.3-r0</TD>\n<TD style='padding: 5px;'>1.8.3-r1</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- Low severity vulnerabilities -->\n\n<h3>Low severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2018-20679</TD>\n<TD style='padding: 5px;'>busybox</TD>\n<TD style='padding: 5px;'>1.28.4-r3</TD>\n<TD style='padding: 5px;'>1.30.0</TD>\n\n</TR>\n</TABLE>\n\n\n<!-- Negligible severity vulnerabilities -->\n\n<h3>Negligible severity vulnerabilities</h3>\n\n<TABLE border='1' style='width: 100%; border-collapse: collapse;'>\n\n<TR>\n<TH style='padding: 5px;'>Vulnerability ID</TH>\n<TH style='padding: 5px;'>Resource name</TH>\n<TH style='padding: 5px;'>Installed version</TH>\n<TH style='padding: 5px;'>Fix version</TH>\n\n</TR>\n<TR>\n<TD style='padding: 5px;'>CVE-2019-5747</TD>\n<TD style='padding: 5px;'>busybox</TD>\n<TD style='padding: 5px;'>1.28.4-r3</TD>\n<TD style='padding: 5px;'>none</TD>\n\n</TR>\n</TABLE>\n\n\n\n"
  },
  {
    "path": "regoservice/testdata/goldens/vuls-slack.golden",
    "content": "[{\"text\":{\"text\":\"Image name: all-in-one:3.5.19223\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"text\":{\"text\":\"Registry: Aqua\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"text\":{\"text\":\"Image is compliant\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"text\":{\"text\":\"Malware found: Yes\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"text\":{\"text\":\"Sensitive data found: Yes\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*CRITICAL*\",\"type\":\"mrkdwn\"},{\"text\":\"*1*\",\"type\":\"mrkdwn\"},{\"text\":\"*HIGH*\",\"type\":\"mrkdwn\"},{\"text\":\"*1*\",\"type\":\"mrkdwn\"},{\"text\":\"*MEDIUM*\",\"type\":\"mrkdwn\"},{\"text\":\"*1*\",\"type\":\"mrkdwn\"},{\"text\":\"*LOW*\",\"type\":\"mrkdwn\"},{\"text\":\"*1*\",\"type\":\"mrkdwn\"},{\"text\":\"*NEGLIGIBLE*\",\"type\":\"mrkdwn\"},{\"text\":\"*1*\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*Assurance controls*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*#* *Control*\",\"type\":\"mrkdwn\"},{\"text\":\"*Policy Name* / *Status*\",\"type\":\"mrkdwn\"},{\"text\":\"1 malware\",\"type\":\"mrkdwn\"},{\"text\":\"Default / PASS\",\"type\":\"mrkdwn\"},{\"text\":\"2 license\",\"type\":\"mrkdwn\"},{\"text\":\"Default / PASS\",\"type\":\"mrkdwn\"},{\"text\":\"3 max_severity\",\"type\":\"mrkdwn\"},{\"text\":\"Default / PASS\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*Found vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"text\":{\"text\":\"*critical severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2018-1000517\",\"type\":\"mrkdwn\"},{\"text\":\"busybox/1.28.4-r3/1.29.0\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*high severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2021-33560\",\"type\":\"mrkdwn\"},{\"text\":\"libgcrypt/1.8.3-r0/1.8.8\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*medium severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2019-12904\",\"type\":\"mrkdwn\"},{\"text\":\"libgcrypt/1.8.3-r0/1.8.3-r1\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*low severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2018-20679\",\"type\":\"mrkdwn\"},{\"text\":\"busybox/1.28.4-r3/1.30.0\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"*negligible severity vulnerabilities*\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*Vulnerability ID*\",\"type\":\"mrkdwn\"},{\"text\":\"*Resource name / Installed version / Fix version*\",\"type\":\"mrkdwn\"},{\"text\":\"CVE-2019-5747\",\"type\":\"mrkdwn\"},{\"text\":\"busybox/1.28.4-r3/none\",\"type\":\"mrkdwn\"}],\"type\":\"section\"},{\"text\":{\"text\":\"Malware\",\"type\":\"mrkdwn\"},\"type\":\"section\"},{\"fields\":[{\"text\":\"*# Malware*\",\"type\":\"mrkdwn\"},{\"text\":\"*Hash / Path*\",\"type\":\"mrkdwn\"}],\"type\":\"section\"}]"
  },
  {
    "path": "regoservice/testdata/inputs/aqua-incident-input.json",
    "content": "{\n  \"create_time\": 0,\n  \"event_category\": \"incident\",\n  \"type\": \"Malware\",\n  \"host_name\": \"ubuntu\",\n  \"host_id\": \"test\",\n  \"category\": \"Test\",\n  \"result\": 2,\n  \"severity_score\": 3,\n  \"name\": \"test\",\n  \"response_policy_name\": \"test\",\n  \"data\": \"{\\\"host\\\": \\\"ubuntu\\\",\\\"rule\\\": \\\"Test-Default-Policy\\\",\\\"level\\\": \\\"block\\\",\\\"action\\\": \\\"test malware delete\\\",\\\"hostid\\\": \\\"host\\\",\\\"hostip\\\": \\\"10.100.102.19\\\",\\\"reason\\\": \\\"Malware detection\\\",\\\"result\\\": 2,\\\"tactic\\\": \\\"Defense Evasion, Execution, Privilege Escalation, Initial Access\\\",\\\"control\\\": \\\"Malware scanning Control\\\",\\\"malware\\\": \\\"Eicar-Test-Signature\\\",\\\"subtype\\\": \\\"malware protection\\\",\\\"category\\\": \\\"malware\\\",\\\"resource\\\": \\\"/home/usr/tmp/malware/eicar.test\\\",\\\"severity\\\": 4,\\\"rule_type\\\": \\\"host.runtime.policy (Test)\\\",\\\"technique\\\": \\\"Execution Guardrails, Exploit Public Facing Application, Client Execution, Privilege Escalation, Remote Services\\\",\\\"k8s_cluster\\\": \\\"Cluster-Test\\\",\\\"malware_type\\\": \\\"Virus\\\",\\\"resource_digest\\\": \\\"0000\\\",\\\"malware_scan_type\\\": \\\"file\\\"}\",\n  \"application_scope\": [\n    \"scope1\",\n    \"scope2\"    ]\n}"
  },
  {
    "path": "regoservice/testdata/inputs/aqua-input.json",
    "content": "{\n  \"image\": \"all-in-one:3.5.19223\",\n  \"registry\": \"Aqua\",\n  \"scan_started\": {\n    \"seconds\": 1624544066,\n    \"nanos\": 881635578\n  },\n  \"scan_duration\": 3,\n  \"pull_skipped\": true,\n  \"image_size\": 178041649,\n  \"digest\": \"sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf\",\n  \"os\": \"alpine\",\n  \"version\": \"3.8.4\",\n  \"resources\": [\n    {\n      \"resource\": {\n        \"format\": \"apk\",\n        \"name\": \"libgcrypt\",\n        \"version\": \"1.8.3-r0\",\n        \"arch\": \"x86_64\",\n        \"cpe\": \"pkg:/alpine:3.8.4:libgcrypt:1.8.3-r0\",\n        \"license\": \"LGPL\",\n        \"layer_digest\": \"sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785\",\n        \"src_name\": \"libgcrypt\",\n        \"src_version\": \"1.8.3-r0\"\n      },\n      \"scanned\": true,\n      \"vulnerabilities\": [\n        {\n          \"name\": \"CVE-2019-12904\",\n          \"description\": \"** DISPUTED ** In Libgcrypt 1.8.4, the C implementation of AES is vulnerable to a flush-and-reload side-channel attack because physical addresses are available to other processes. (The C implementation is used on platforms where an assembly-language implementation is unavailable.) NOTE: the vendor's position is that the issue report cannot be validated because there is no description of an attack.\",\n          \"nvd_score\": 4.3,\n          \"nvd_score_version\": \"CVSS v2\",\n          \"nvd_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n          \"nvd_severity\": \"medium\",\n          \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-12904\",\n          \"vendor_score_version\": \"CVSS v2\",\n          \"publish_date\": \"2019-06-20\",\n          \"modification_date\": \"2021-03-04\",\n          \"fix_version\": \"1.8.3-r1\",\n          \"solution\": \"Upgrade package libgcrypt to version 1.8.3-r1 or above.\",\n          \"nvd_score_v3\": 5.9,\n          \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"nvd_severity_v3\": \"medium\",\n          \"aqua_score\": 4.3,\n          \"aqua_severity\": \"medium\",\n          \"aqua_vectors\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n          \"aqua_scoring_system\": \"CVSS V2\",\n          \"heuristic_ref_id\": 328557,\n          \"aqua_severity_classification\": \"NVD CVSS V2 Score: 4.3\",\n          \"aqua_score_classification\": \"NVD CVSS V2 Score: 4.3\"\n        },\n        {\n          \"name\": \"CVE-2021-33560\",\n          \"description\": \"Libgcrypt before 1.8.8 and 1.9.x before 1.9.3 mishandles ElGamal encryption because it lacks exponent blinding to address a side-channel attack against mpi_powm, and the window size is not chosen appropriately. (There is also an interoperability problem because the selection of the k integer value does not properly consider the differences between basic ElGamal encryption and generalized ElGamal encryption.) This, for example, affects use of ElGamal in OpenPGP.\",\n          \"nvd_score\": 5,\n          \"nvd_score_version\": \"CVSS v2\",\n          \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n          \"nvd_severity\": \"high\",\n          \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-33560\",\n          \"publish_date\": \"2021-06-08\",\n          \"modification_date\": \"2021-06-15\",\n          \"fix_version\": \"1.8.8\",\n          \"nvd_score_v3\": 7.5,\n          \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"nvd_severity_v3\": \"high\",\n          \"aqua_score\": 5,\n          \"aqua_severity\": \"high\",\n          \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n          \"aqua_scoring_system\": \"CVSS V2\",\n          \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n          \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n        }\n      ]\n    },\n    {\n      \"resource\": {\n        \"format\": \"apk\",\n        \"name\": \"busybox\",\n        \"version\": \"1.28.4-r3\",\n        \"arch\": \"x86_64\",\n        \"cpe\": \"pkg:/alpine:3.8.4:busybox:1.28.4-r3\",\n        \"license\": \"GPL2\",\n        \"layer_digest\": \"sha256:c87736221ed0bcaa60b8e92a19bec2284899ef89226f2a07968677cf59e637a4\",\n        \"src_name\": \"busybox\",\n        \"src_version\": \"1.28.4-r3\"\n      },\n      \"scanned\": true,\n      \"vulnerabilities\": [\n        {\n          \"name\": \"CVE-2018-1000517\",\n          \"description\": \"BusyBox project BusyBox wget version prior to commit 8e2174e9bd836e53c8b9c6e00d1bc6e2a718686e contains a Buffer Overflow vulnerability in Busybox wget that can result in heap buffer overflow. This attack appear to be exploitable via network connectivity. This vulnerability appears to have been fixed in after commit 8e2174e9bd836e53c8b9c6e00d1bc6e2a718686e.\",\n          \"nvd_score\": 7.5,\n          \"nvd_score_version\": \"CVSS v2\",\n          \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n          \"nvd_severity\": \"critical\",\n          \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1000517\",\n          \"publish_date\": \"2018-06-26\",\n          \"modification_date\": \"2021-02-18\",\n          \"fix_version\": \"1.29.0\",\n          \"nvd_score_v3\": 9.8,\n          \"nvd_vectors_v3\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n          \"nvd_severity_v3\": \"critical\",\n          \"aqua_score\": 7.5,\n          \"aqua_severity\": \"critical\",\n          \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n          \"aqua_scoring_system\": \"CVSS V2\",\n          \"aqua_severity_classification\": \"NVD CVSS V2 Score: 7.5\",\n          \"aqua_score_classification\": \"NVD CVSS V2 Score: 7.5\"\n        },\n        {\n          \"name\": \"CVE-2018-20679\",\n          \"description\": \"An issue was discovered in BusyBox before 1.30.0. An out of bounds read in udhcp components (consumed by the DHCP server, client, and relay) allows a remote attacker to leak sensitive information from the stack by sending a crafted DHCP message. This is related to verification in udhcp_get_option() in networking/udhcp/common.c that 4-byte options are indeed 4 bytes.\",\n          \"nvd_score\": 5,\n          \"nvd_score_version\": \"CVSS v2\",\n          \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n          \"nvd_severity\": \"low\",\n          \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-20679\",\n          \"publish_date\": \"2019-01-09\",\n          \"modification_date\": \"2019-09-04\",\n          \"fix_version\": \"1.30.0\",\n          \"nvd_score_v3\": 7.5,\n          \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"nvd_severity_v3\": \"low\",\n          \"aqua_score\": 5,\n          \"aqua_severity\": \"low\",\n          \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n          \"aqua_scoring_system\": \"CVSS V2\",\n          \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n          \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n        },\n        {\n          \"name\": \"CVE-2019-5747\",\n          \"description\": \"An issue was discovered in BusyBox through 1.30.0. An out of bounds read in udhcp components (consumed by the DHCP server, client, and/or relay) might allow a remote attacker to leak sensitive information from the stack by sending a crafted DHCP message. This is related to assurance of a 4-byte length when decoding DHCP_SUBNET. NOTE: this issue exists because of an incomplete fix for CVE-2018-20679.\",\n          \"nvd_score\": 5,\n          \"nvd_score_version\": \"CVSS v2\",\n          \"nvd_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n          \"nvd_severity\": \"negligible\",\n          \"nvd_url\": \"https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-5747\",\n          \"publish_date\": \"2019-01-09\",\n          \"modification_date\": \"2019-09-04\",\n          \"already_acknowledged\": true,\n          \"nvd_score_v3\": 7.5,\n          \"nvd_vectors_v3\": \"CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\",\n          \"nvd_severity_v3\": \"negligible\",\n          \"aqua_score\": 5,\n          \"aqua_severity\": \"negligible\",\n          \"aqua_vectors\": \"AV:N/AC:L/Au:N/C:P/I:N/A:N\",\n          \"aqua_scoring_system\": \"CVSS V2\",\n          \"aqua_severity_classification\": \"NVD CVSS V2 Score: 5.0\",\n          \"aqua_score_classification\": \"NVD CVSS V2 Score: 5.0\"\n        }\n      ]\n    }\n  ],\n  \"image_assurance_results\": {\n    \"checks_performed\": [\n      {\n        \"policy_id\": 1,\n        \"policy_name\": \"Default\",\n        \"control\": \"malware\"\n      },\n      {\n        \"policy_id\": 1,\n        \"policy_name\": \"Default\",\n        \"control\": \"license\"\n      },\n      {\n        \"policy_id\": 1,\n        \"policy_name\": \"Default\",\n        \"control\": \"max_severity\",\n        \"maximum_severity_allowed\": \"critical\",\n        \"maximum_severity_found\": \"high\"\n      }\n    ]\n  },\n  \"vulnerability_summary\": {\n    \"total\": 5,\n    \"critical\": 1,\n    \"high\": 1,\n    \"medium\": 1,\n    \"low\": 1,\n    \"negligible\": 1,\n    \"sensitive\": 0,\n    \"malware\": 0,\n    \"score_average\": 5.020931\n  },\n  \"scan_options\": {\n    \"scan_executables\": true,\n    \"scan_sensitive_data\": true,\n    \"show_will_not_fix\": true,\n    \"webhook_url\": \"https://975cb1e5b1fc.ngrok.io\",\n    \"scan_malware\": true,\n    \"strict_scan\": true,\n    \"scan_files\": true,\n    \"scan_timeout\": 3600000000000,\n    \"manual_pull_fallback\": true,\n    \"dockerless\": true,\n    \"enable_fast_scanning\": true,\n    \"memoryThrottling\": true,\n    \"suggest_os_upgrade\": true,\n    \"seim_enabled\": true\n  },\n  \"previous_digest\": \"sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf\",\n  \"vulnerability_diff\": {\n    \"total\": 0,\n    \"critical\": 0,\n    \"high\": 0,\n    \"medium\": 0,\n    \"low\": 0,\n    \"negligible\": 0,\n    \"sensitive\": 0,\n    \"malware\": 0\n  },\n  \"initiating_user\": \"upwork\",\n  \"data_date\": 1624490283,\n  \"pull_name\": \"registry.aquasec.com/all-in-one:3.5.19223\",\n  \"changed_result\": false,\n  \"function_metadata\": {},\n  \"scan_id\": 386815,\n  \"required_image_platform\": \"amd64:::\",\n  \"scanned_image_platform\": \"amd64::linux:\",\n  \"security_feeds_used\": {\n    \"executables\": \"92475757e80429\"\n  },\n  \"image_id\": 704,\n  \"internal_digest_id\": {\n    \"id\": 1095\n  },\n  \"application_scope\": [\n    \"group\"\n  ],\n  \"application_scope_owners\": [\n    \"owner\"\n  ]\n}\n\n"
  },
  {
    "path": "regoservice/testdata/inputs/aqua-insight-input.json",
    "content": "{\n  \"account_id\": \"4700\",\n  \"customTriggerType\": \"custom-insight\",\n  \"evidence\": {\n    \"malware\": null,\n    \"malware_remediation\": \"\",\n    \"privileged_iam_roles\": null,\n    \"privileged_iam_roles_remediation\": null,\n    \"privileged_workloads\": null,\n    \"privileged_workloads_remediation\": null,\n    \"sensitive_data\": [\n      {\n        \"file_name\": \"\",\n        \"file_path\": \"/private-key.pem\",\n        \"file_type\": \"RSA PRIVATE KEY\",\n        \"function\": \"\",\n        \"image\": \"shayyo/sensitive_data:latest\"            }\n    ],\n    \"sensitive_data_remediation\": \"\",\n    \"vulnerabilities\": null,\n    \"vulnerabilities_remediation\": null    },\n  \"global_id\": \"bab1e6eb-5017-42f3-b419-5db95464e443\",\n  \"id\": \"6131180\",\n  \"insight\": {\n    \"category\": \"\",\n    \"description\": \"Workloads or images containing login data\",\n    \"id\": \"aqua-3006\",\n    \"impact\": \"Attackers with access to this workload or image might be able to use the login data to gain initial access to other resources\",\n    \"instance\": 0,\n    \"priority\": 2,\n    \"rank\": 380,\n    \"suppress_resources\": 0,\n    \"suppress_resources_with_expiration\": 0,\n    \"total_impacted_resources\": 0    },\n  \"postee\": {\n    \"AquaServer\": \"\"    },\n  \"resource\": {\n    \"arn\": \"\",\n    \"cluster_arn\": \"\",\n    \"comment\": \"\",\n    \"found_date\": \"2022-08-25T09:02:28.991Z\",\n    \"full_path\": \"\",\n    \"global_id\": \"3614f379-0ac0-4074-8bb7-8bc7b1fd5b26\",\n    \"id\": \"6131180\",\n    \"internet_exposure_status\": \"unknown\",\n    \"is_connected\": false,\n    \"is_suppressed\": false,\n    \"last_scanned\": \"2022-08-25T08:59:42.314673Z\",\n    \"name\": \"shayyo/sensitive_data:latest\",\n    \"num_pods\": 0,\n    \"short_path\": \"\",\n    \"steps\": {\n      \"Image\": \"sensitive_data:latest\",\n      \"Registry\": \"Docker Hub\",\n      \"RegistryDomain\": \"docker.io\"        },\n    \"suppress\": {\n      \"configuration\": {\n        \"description\": \"\",\n        \"duration_days\": 0,\n        \"user\": \"\",\n        \"with_expiration\": false            },\n      \"date\": \"0001-01-01T00:00:00Z\",\n      \"expiration_date\": \"0001-01-01T00:00:00Z\"        }\n  },\n  \"resourceTypeKey\": \"\",\n  \"application_scope\": [\"scope1\",\"scope2\"],\n  \"response_policy_id\": \"11\",\n  \"response_policy_name\": \"insights_login data\",\n  \"type\": \"Image\"}"
  },
  {
    "path": "regoservice/testdata/inputs/simple-input.json",
    "content": "{\n  \"user\": \"demo\"\n}"
  },
  {
    "path": "regoservice/testdata/inputs/trivy-input.json",
    "content": "{\n  \"SchemaVersion\": 2,\n  \"ArtifactName\": \"pom.xml\",\n  \"ArtifactType\": \"filesystem\",\n  \"Metadata\": {\n    \"ImageConfig\": {\n      \"architecture\": \"\",\n      \"created\": \"0001-01-01T00:00:00Z\",\n      \"os\": \"\",\n      \"rootfs\": {\n        \"type\": \"\",\n        \"diff_ids\": null\n      },\n      \"config\": {}\n    }\n  },\n  \"Results\": [\n    {\n      \"Target\": \"pom.xml\",\n      \"Class\": \"lang-pkgs\",\n      \"Type\": \"pom\",\n      \"Vulnerabilities\": [\n        {\n          \"VulnerabilityID\": \"CVE-2019-17571\",\n          \"PkgName\": \"log4j:log4j\",\n          \"InstalledVersion\": \"1.2.17\",\n          \"FixedVersion\": \"2.0-alpha1\",\n          \"Status\": \"fixed\",\n          \"Layer\": {},\n          \"SeveritySource\": \"nvd\",\n          \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2019-17571\",\n          \"DataSource\": {\n            \"ID\": \"glad\",\n            \"Name\": \"GitLab Advisory Database Community\",\n            \"URL\": \"https://gitlab.com/gitlab-org/advisories-community\"\n          },\n          \"Title\": \"log4j: deserialization of untrusted data in SocketServer\",\n          \"Description\": \"Included in Log4j 1.2 is a SocketServer class that is vulnerable to deserialization of untrusted data which can be exploited to remotely execute arbitrary code when combined with a deserialization gadget when listening to untrusted network traffic for log data. This affects Log4j versions up to 1.2 up to 1.2.17.\",\n          \"Severity\": \"CRITICAL\",\n          \"CweIDs\": [\n            \"CWE-502\"\n          ],\n          \"CVSS\": {\n            \"ghsa\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 9.8\n            },\n            \"nvd\": {\n              \"V2Vector\": \"AV:N/AC:L/Au:N/C:P/I:P/A:P\",\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n              \"V2Score\": 7.5,\n              \"V3Score\": 9.8\n            },\n            \"redhat\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 9.8\n            }\n          },\n          \"References\": [\n            \"http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00022.html\",\n            \"https://access.redhat.com/security/cve/CVE-2019-17571\",\n            \"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-17571\",\n            \"https://github.com/advisories/GHSA-2qrg-x229-3v8q\",\n            \"https://lists.apache.org/thread.html/277b4b5c2b0e06a825ccec565fa65bd671f35a4d58e3e2ec5d0618e1@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/44491fb9cc19acc901f7cff34acb7376619f15638439416e3e14761c@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/479471e6debd608c837b9815b76eab24676657d4444fcfd5ef96d6e6@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/564f03b4e9511fcba29c68fc0299372dadbdb002718fa8edcc4325e4@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/6114ce566200d76e3cc45c521a62c2c5a4eac15738248f58a99f622c@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/752ec92cd1e334a639e79bfbd689a4ec2c6579ec5bb41b53ffdf358d@%3Cdev.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/8ab32b4c9f1826f20add7c40be08909de9f58a89dc1de9c09953f5ac@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/eea03d504b36e8f870e8321d908e1def1addda16adda04327fe7c125%40%3Cdev.logging.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r05755112a8c164abc1004bb44f198b1e3d8ca3d546a8f13ebd3aa05f@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r107c8737db39ec9ec4f4e7147b249e29be79170b9ef4b80528105a2d@%3Cdev.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r13d4b5c60ff63f3c4fab51d6ff266655be503b8a1884e2f2fab67c3a@%3Ccommon-issues.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r189aaeaad897f7d6b96f7c43a8ef2dfb9f6e9f8c1cc9ad182ce9b9ae@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r18f1c010b554a3a2d761e8ffffd8674fd4747bcbcf16c643d708318c@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r1b103833cb5bc8466e24ff0ecc5e75b45a705334ab6a444e64e840a0@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r1b7734dfdfd938640f2f5fb6f4231a267145c71ed60cc7faa1cbac07@%3Ccommon-issues.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r26244f9f7d9a8a27a092eb0b2a0ca9395e88fcde8b5edaeca7ce569c@%3Ccommon-issues.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r2721aba31a8562639c4b937150897e24f78f747cdbda8641c0f659fe@%3Cusers.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r2756fd570b6709d55a61831ca028405bcb3e312175a60bc5d911c81f@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r2ce8d26154bea939536e6cf27ed02d3192bf5c5d04df885a80fe89b3@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r2ff63f210842a3c5e42f03a35d8f3a345134d073c80a04077341c211@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r3543ead2317dcd3306f69ee37b07dd383dbba6e2f47ff11eb55879ad@%3Cusers.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r356d57d6225f91fdc30f8b0a2bed229d1ece55e16e552878c5fa809a@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r3784834e80df2f284577a5596340fb84346c91a2dea6a073e65e3397@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r3a85514a518f3080ab1fc2652cfe122c2ccf67cfb32356acb1b08fe8@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r3bf7b982dfa0779f8a71f843d2aa6b4184a53e6be7f149ee079387fd@%3Cdev.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r3c575cabc7386e646fb12cb82b0b38ae5a6ade8a800f827107824495@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r3cf50d05ce8cec8c09392624b7bae750e7643dae60ef2438641ee015@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r3d666e4e8905157f3c046d31398b04f2bfd4519e31f266de108c6919@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r48d5019bd42e0770f7e5351e420a63a41ff1f16924942442c6aff6a8@%3Ccommits.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r48efc7cb5aeb4e1f67aaa06fb4b5479a5635d12f07d0b93fc2d08809@%3Ccommits.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r4ac89cbecd9e298ae9fafb5afda6fa77ac75c78d1ac957837e066c4e@%3Cuser.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r4b25538be50126194cc646836c718b1a4d8f71bd9c912af5b59134ad@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r52a5129df402352adc34d052bab9234c8ef63596306506a89fdc7328@%3Cusers.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r594411f4bddebaf48a4c70266d0b7849e0d82bb72826f61b3a35bba7@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r5c084578b3e3b40bd903c9d9e525097421bcd88178e672f612102eb2@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r61590890edcc64140e0c606954b29a063c3d08a2b41d447256d51a78@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r61db8e7dcb56dc000a5387a88f7a473bacec5ee01b9ff3f55308aacc@%3Cdev.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r61db8e7dcb56dc000a5387a88f7a473bacec5ee01b9ff3f55308aacc@%3Cusers.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r6236b5f8646d48af8b66d5050f288304016840788e508c883356fe0e@%3Clog4j-user.logging.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r681b4432d0605f327b68b9f8a42662993e699d04614de4851c35ffd1@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r696507338dd5f44efc23d98cafe30f217cf3ba78e77ed1324c7a5179@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r6aec6b8f70167fa325fb98b3b5c9ce0ffaed026e697b69b85ac24628@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r6b45a2fcc8e98ac93a179183dbb7f340027bdb8e3ab393418076b153@%3Ccommon-issues.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r6d34da5a0ca17ab08179a30c971446c7421af0e96f6d60867eabfc52@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r71e26f9c2d5826c6f95ad60f7d052d75e1e70b0d2dd853db6fc26d5f@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r746fbc3fc13aee292ae6851f7a5080f592fa3a67b983c6887cdb1fc5@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r7a1acc95373105169bd44df710c2f462cad31fb805364d2958a5ee03@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r7bcdc710857725c311b856c0b82cee6207178af5dcde1bd43d289826@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r7f462c69d5ded4c0223e014d95a3496690423c5f6f05c09e2f2a407a@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8244fd0831db894d5e89911ded9c72196d395a90ae655414d23ed0dd@%3Cusers.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8418a0dff1729f19cf1024937e23a2db4c0f94f2794a423f5c10e8e7@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8890b8f18f1de821595792b58b968a89692a255bc20d86d395270740@%3Ccommits.druid.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8a1cfd4705258c106e488091fcec85f194c82f2bbde6bd151e201870@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8c392ca48bb7e50754e4bc05865e9731b23d568d18a520fe3d8c1f75@%3Ccommon-issues.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8c6300245c0bcef095e9f07b48157e2c6471df0816db3408fcf1d748@%3Ccommon-issues.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8d78a0fbb56d505461e29868d1026e98c402e6a568c13a6da67896a2@%3Cdev.jena.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8e3f7da12bf5750b0a02e69a78a61073a2ac950eed7451ce70a65177@%3Ccommits.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r909b8e3a36913944d3b7bafe9635d4ca84f8f0e2cd146a1784f667c2@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r90c23eb8c82835fa82df85ae5e88c81fd9241e20a22971b0fb8f2c34@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r944183c871594fe9a555b8519a7c945bbcf6714d72461aa6c929028f@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r9a9e3b42cd5d1c4536a14ef04f75048dec8e2740ac6a138ea912177f@%3Cpluto-dev.portals.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r9d0d03f2e7d9e13c68b530f81d02b0fec33133edcf27330d8089fcfb@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r9d2e28e71f91ba0b6f4114c8ecd96e2b1f7e0d06bdf8eb768c183aa9@%3Ccommon-issues.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r9dc2505651788ac668299774d9e7af4dc616be2f56fdc684d1170882@%3Cusers.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r9fb3238cfc3222f2392ca6517353aadae18f76866157318ac562e706@%3Ccommon-issues.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/ra18a903f785aed9403aea38bc6f36844a056283c00dcfc6936b6318c@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/ra38785cfc0e7f17f8e24bebf775dd032c033fadcaea29e5bc9fffc60@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/ra54fa49be3e773d99ccc9c2a422311cf77e3ecd3b8594ee93043a6b1@%3Cdev.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/ra9611a8431cb62369bce8909d7645597e1dd45c24b448836b1e54940@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/raedd12dc24412b3780432bf202a2618a21a727788543e5337a458ead@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rb1b29aee737e1c37fe1d48528cb0febac4f5deed51f5412e6fdfe2bf@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rb3c94619728c8f8c176d8e175e0a1086ca737ecdfcd5a2214bb768bc@%3Ccommits.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rbc45eb0f53fd6242af3e666c2189464f848a851d408289840cecc6e3@%3Ccommits.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rbd19de368abf0764e4383ec44d527bc9870176f488a494f09a40500d@%3Ccommon-dev.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rbdf18e39428b5c80fc35113470198b1fe53b287a76a46b0f8780b5fd@%3Cdev.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rbf4ce74b0d1fa9810dec50ba3ace0caeea677af7c27a97111c06ccb7@%3Cdev.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rbf4ce74b0d1fa9810dec50ba3ace0caeea677af7c27a97111c06ccb7@%3Cusers.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rc17d8491beee51607693019857e41e769795366b85be00aa2f4b3159@%3Cnotifications.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rc1eaed7f7d774d5d02f66e49baced31e04827a1293d61a70bd003ca7@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rc628307962ae1b8cc2d21b8e4b7dd6d7755b2dd52fa56a151a27e4fd@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rca24a281000fb681d7e26e5c031a21eb4b0593a7735f781b53dae4e2@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rcd71280585425dad7e232f239c5709e425efdd0d3de4a92f808a4767@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd0e44e8ef71eeaaa3cf3d1b8b41eb25894372e2995ec908ce7624d26@%3Ccommits.pulsar.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd3a9511eebab60e23f224841390a3f8cd5358cff605c5f7042171e47@%3Cdev.tinkerpop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd5dbeee4808c0f2b9b51479b50de3cc6adb1072c332a200d9107f13e@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd6254837403e8cbfc7018baa9be29705f3f06bd007c83708f9a97679@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd7805c1bf9388968508c6c8f84588773216e560055ddcc813d19f347@%3Ccommon-issues.hadoop.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd882ab6b642fe59cbbe94dc02bd197342058208f482e57b537940a4b@%3Cpluto-dev.portals.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rda4849c6823dd3e83c7a356eb883180811d5c28359fe46865fd151c3@%3Cusers.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rdb7ddf28807e27c7801f6e56a0dfb31092d34c61bdd4fa2de9182119@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rdec0d8ac1f03e6905b0de2df1d5fcdb98b94556e4f6cccf7519fdb26@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rdf2a0d94c3b5b523aeff7741ae71347415276062811b687f30ea6573@%3Ccommits.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/re36da78e4f3955ba6c1c373a2ab85a4deb215ca74b85fcd66142fea1@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/re8c21ed9dd218c217d242ffa90778428e446b082b5e1c29f567e8374@%3Cissues.activemq.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/reaf6b996f74f12b4557bc221abe88f58270ac583942fa41293c61f94@%3Cpluto-scm.portals.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rec34b1cccf907898e7cb36051ffac3ccf1ea89d0b261a2a3b3fb267f@%3Ccommits.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rf1b434e11834a4449cd7addb69ed0aef0923112b5938182b363a968c@%3Cnotifications.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rf2567488cfc9212b42e34c6393cfa1c14e30e4838b98dda84d71041f@%3Cdev.tika.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rf53eeefb7e7e524deaacb9f8671cbf01b8a253e865fb94e7656722c0@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rf77f79699c8d7e430c14cf480f12ed1297e6e8cf2ed379a425941e80@%3Cpluto-dev.portals.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rf9c19bcc2f7a98a880fa3e3456c003d331812b55836b34ef648063c9@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rfdf65fa675c64a64459817344e0e6c44d51ee264beea6e5851fb60dc@%3Cissues.bookkeeper.apache.org%3E\",\n            \"https://lists.debian.org/debian-lts-announce/2020/01/msg00008.html\",\n            \"https://nvd.nist.gov/vuln/detail/CVE-2019-17571\",\n            \"https://security.netapp.com/advisory/ntap-20200110-0001/\",\n            \"https://ubuntu.com/security/notices/USN-4495-1\",\n            \"https://ubuntu.com/security/notices/USN-5998-1\",\n            \"https://usn.ubuntu.com/4495-1/\",\n            \"https://www.cve.org/CVERecord?id=CVE-2019-17571\",\n            \"https://www.debian.org/security/2020/dsa-4686\",\n            \"https://www.oracle.com/security-alerts/cpuApr2021.html\",\n            \"https://www.oracle.com/security-alerts/cpuapr2020.html\",\n            \"https://www.oracle.com/security-alerts/cpuapr2022.html\",\n            \"https://www.oracle.com/security-alerts/cpujul2020.html\",\n            \"https://www.oracle.com/security-alerts/cpujul2022.html\"\n          ],\n          \"PublishedDate\": \"2019-12-20T17:15:00Z\",\n          \"LastModifiedDate\": \"2022-12-14T17:50:00Z\"\n        },\n        {\n          \"VulnerabilityID\": \"CVE-2022-23305\",\n          \"PkgName\": \"log4j:log4j\",\n          \"InstalledVersion\": \"1.2.17\",\n          \"Status\": \"affected\",\n          \"Layer\": {},\n          \"SeveritySource\": \"nvd\",\n          \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2022-23305\",\n          \"DataSource\": {\n            \"ID\": \"glad\",\n            \"Name\": \"GitLab Advisory Database Community\",\n            \"URL\": \"https://gitlab.com/gitlab-org/advisories-community\"\n          },\n          \"Title\": \"log4j: SQL injection in Log4j 1.x when application is configured to use JDBCAppender\",\n          \"Description\": \"By design, the JDBCAppender in Log4j 1.2.x accepts an SQL statement as a configuration parameter where the values to be inserted are converters from PatternLayout. The message converter, %m, is likely to always be included. This allows attackers to manipulate the SQL by entering crafted strings into input fields or headers of an application that are logged allowing unintended SQL queries to be executed. Note this issue only affects Log4j 1.x when specifically configured to use the JDBCAppender, which is not the default. Beginning in version 2.0-beta8, the JDBCAppender was re-introduced with proper support for parameterized SQL queries and further customization over the columns written to in logs. Apache Log4j 1.2 reached end of life in August 2015. Users should upgrade to Log4j 2 as it addresses numerous other issues from the previous versions.\",\n          \"Severity\": \"CRITICAL\",\n          \"CweIDs\": [\n            \"CWE-89\"\n          ],\n          \"CVSS\": {\n            \"ghsa\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 9.8\n            },\n            \"nvd\": {\n              \"V2Vector\": \"AV:N/AC:M/Au:N/C:P/I:P/A:P\",\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n              \"V2Score\": 6.8,\n              \"V3Score\": 9.8\n            },\n            \"redhat\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 8.8\n            }\n          },\n          \"References\": [\n            \"http://www.openwall.com/lists/oss-security/2022/01/18/4\",\n            \"https://access.redhat.com/security/cve/CVE-2022-23305\",\n            \"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-23305\",\n            \"https://errata.almalinux.org/8/ALSA-2022-0290.html\",\n            \"https://github.com/advisories/GHSA-65fg-84f6-3jq3\",\n            \"https://linux.oracle.com/cve/CVE-2022-23305.html\",\n            \"https://linux.oracle.com/errata/ELSA-2022-9419.html\",\n            \"https://lists.apache.org/thread/pt6lh3pbsvxqlwlp4c5l798dv2hkc85y\",\n            \"https://logging.apache.org/log4j/1.2/index.html\",\n            \"https://nvd.nist.gov/vuln/detail/CVE-2022-23305\",\n            \"https://security.netapp.com/advisory/ntap-20220217-0007/\",\n            \"https://ubuntu.com/security/notices/USN-5998-1\",\n            \"https://www.cve.org/CVERecord?id=CVE-2022-23305\",\n            \"https://www.openwall.com/lists/oss-security/2022/01/18/4\",\n            \"https://www.oracle.com/security-alerts/cpuapr2022.html\",\n            \"https://www.oracle.com/security-alerts/cpujul2022.html\"\n          ],\n          \"PublishedDate\": \"2022-01-18T16:15:00Z\",\n          \"LastModifiedDate\": \"2023-02-24T15:30:00Z\"\n        },\n        {\n          \"VulnerabilityID\": \"CVE-2021-4104\",\n          \"PkgName\": \"log4j:log4j\",\n          \"InstalledVersion\": \"1.2.17\",\n          \"Status\": \"affected\",\n          \"Layer\": {},\n          \"SeveritySource\": \"nvd\",\n          \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2021-4104\",\n          \"DataSource\": {\n            \"ID\": \"glad\",\n            \"Name\": \"GitLab Advisory Database Community\",\n            \"URL\": \"https://gitlab.com/gitlab-org/advisories-community\"\n          },\n          \"Title\": \"Remote code execution in Log4j 1.x when application is configured to use JMSAppender\",\n          \"Description\": \"JMSAppender in Log4j 1.2 is vulnerable to deserialization of untrusted data when the attacker has write access to the Log4j configuration. The attacker can provide TopicBindingName and TopicConnectionFactoryBindingName configurations causing JMSAppender to perform JNDI requests that result in remote code execution in a similar fashion to CVE-2021-44228. Note this issue only affects Log4j 1.2 when specifically configured to use JMSAppender, which is not the default. Apache Log4j 1.2 reached end of life in August 2015. Users should upgrade to Log4j 2 as it addresses numerous other issues from the previous versions.\",\n          \"Severity\": \"HIGH\",\n          \"CweIDs\": [\n            \"CWE-502\"\n          ],\n          \"CVSS\": {\n            \"ghsa\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 8.1\n            },\n            \"nvd\": {\n              \"V2Vector\": \"AV:N/AC:M/Au:S/C:P/I:P/A:P\",\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"V2Score\": 6,\n              \"V3Score\": 7.5\n            },\n            \"redhat\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 7.5\n            }\n          },\n          \"References\": [\n            \"http://www.openwall.com/lists/oss-security/2022/01/18/3\",\n            \"https://access.redhat.com/security/cve/CVE-2021-4104\",\n            \"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-4104\",\n            \"https://errata.almalinux.org/8/ALSA-2022-0290.html\",\n            \"https://github.com/advisories/GHSA-fp5r-v3w9-4333\",\n            \"https://github.com/apache/logging-log4j2/pull/608#issuecomment-990494126\",\n            \"https://github.com/apache/logging-log4j2/pull/608#issuecomment-991723301\",\n            \"https://linux.oracle.com/cve/CVE-2021-4104.html\",\n            \"https://linux.oracle.com/errata/ELSA-2022-9056.html\",\n            \"https://lists.apache.org/thread/0x4zvtq92yggdgvwfgsftqrj4xx5w0nx\",\n            \"https://nvd.nist.gov/vuln/detail/CVE-2021-4104\",\n            \"https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0033\",\n            \"https://security.gentoo.org/glsa/202209-02\",\n            \"https://security.netapp.com/advisory/ntap-20211223-0007/\",\n            \"https://ubuntu.com/security/notices/USN-5223-1\",\n            \"https://ubuntu.com/security/notices/USN-5223-2\",\n            \"https://www.cve.org/CVERecord?id=CVE-2021-4104\",\n            \"https://www.cve.org/CVERecord?id=CVE-2021-44228\",\n            \"https://www.kb.cert.org/vuls/id/930724\",\n            \"https://www.openwall.com/lists/oss-security/2021/12/13/1\",\n            \"https://www.openwall.com/lists/oss-security/2021/12/13/2\",\n            \"https://www.oracle.com/security-alerts/cpuapr2022.html\",\n            \"https://www.oracle.com/security-alerts/cpujan2022.html\",\n            \"https://www.oracle.com/security-alerts/cpujul2022.html\"\n          ],\n          \"PublishedDate\": \"2021-12-14T12:15:00Z\",\n          \"LastModifiedDate\": \"2022-10-05T17:53:00Z\"\n        },\n        {\n          \"VulnerabilityID\": \"CVE-2022-23302\",\n          \"PkgName\": \"log4j:log4j\",\n          \"InstalledVersion\": \"1.2.17\",\n          \"Status\": \"affected\",\n          \"Layer\": {},\n          \"SeveritySource\": \"nvd\",\n          \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2022-23302\",\n          \"DataSource\": {\n            \"ID\": \"glad\",\n            \"Name\": \"GitLab Advisory Database Community\",\n            \"URL\": \"https://gitlab.com/gitlab-org/advisories-community\"\n          },\n          \"Title\": \"log4j: Remote code execution in Log4j 1.x when application is configured to use JMSSink\",\n          \"Description\": \"JMSSink in all versions of Log4j 1.x is vulnerable to deserialization of untrusted data when the attacker has write access to the Log4j configuration or if the configuration references an LDAP service the attacker has access to. The attacker can provide a TopicConnectionFactoryBindingName configuration causing JMSSink to perform JNDI requests that result in remote code execution in a similar fashion to CVE-2021-4104. Note this issue only affects Log4j 1.x when specifically configured to use JMSSink, which is not the default. Apache Log4j 1.2 reached end of life in August 2015. Users should upgrade to Log4j 2 as it addresses numerous other issues from the previous versions.\",\n          \"Severity\": \"MEDIUM\",\n          \"CweIDs\": [\n            \"CWE-502\"\n          ],\n          \"CVSS\": {\n            \"ghsa\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 8.8\n            },\n            \"nvd\": {\n              \"V2Vector\": \"AV:N/AC:M/Au:S/C:P/I:P/A:P\",\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"V2Score\": 6,\n              \"V3Score\": 8.8\n            },\n            \"redhat\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 8.8\n            }\n          },\n          \"References\": [\n            \"http://www.openwall.com/lists/oss-security/2022/01/18/3\",\n            \"https://access.redhat.com/security/cve/CVE-2022-23302\",\n            \"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-23302\",\n            \"https://errata.almalinux.org/8/ALSA-2022-0290.html\",\n            \"https://github.com/advisories/GHSA-w9p3-5cr8-m3jj\",\n            \"https://linux.oracle.com/cve/CVE-2022-23302.html\",\n            \"https://linux.oracle.com/errata/ELSA-2022-9419.html\",\n            \"https://lists.apache.org/thread/bsr3l5qz4g0myrjhy9h67bcxodpkwj4w\",\n            \"https://logging.apache.org/log4j/1.2/index.html\",\n            \"https://nvd.nist.gov/vuln/detail/CVE-2022-23302\",\n            \"https://security.netapp.com/advisory/ntap-20220217-0006/\",\n            \"https://ubuntu.com/security/notices/USN-5998-1\",\n            \"https://www.cve.org/CVERecord?id=CVE-2022-23302\",\n            \"https://www.openwall.com/lists/oss-security/2022/01/18/3\",\n            \"https://www.oracle.com/security-alerts/cpuapr2022.html\",\n            \"https://www.oracle.com/security-alerts/cpujul2022.html\"\n          ],\n          \"PublishedDate\": \"2022-01-18T16:15:00Z\",\n          \"LastModifiedDate\": \"2023-02-24T15:30:00Z\"\n        },\n        {\n          \"VulnerabilityID\": \"CVE-2022-23307\",\n          \"PkgName\": \"log4j:log4j\",\n          \"InstalledVersion\": \"1.2.17\",\n          \"Status\": \"affected\",\n          \"Layer\": {},\n          \"SeveritySource\": \"nvd\",\n          \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2022-23307\",\n          \"DataSource\": {\n            \"ID\": \"glad\",\n            \"Name\": \"GitLab Advisory Database Community\",\n            \"URL\": \"https://gitlab.com/gitlab-org/advisories-community\"\n          },\n          \"Title\": \"log4j: Unsafe deserialization flaw in Chainsaw log viewer\",\n          \"Description\": \"CVE-2020-9493 identified a deserialization issue that was present in Apache Chainsaw. Prior to Chainsaw V2.0 Chainsaw was a component of Apache Log4j 1.2.x where the same issue exists.\",\n          \"Severity\": \"HIGH\",\n          \"CweIDs\": [\n            \"CWE-502\"\n          ],\n          \"CVSS\": {\n            \"ghsa\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 9.8\n            },\n            \"nvd\": {\n              \"V2Vector\": \"AV:N/AC:L/Au:S/C:C/I:C/A:C\",\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"V2Score\": 9,\n              \"V3Score\": 8.8\n            },\n            \"redhat\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H\",\n              \"V3Score\": 8.8\n            }\n          },\n          \"References\": [\n            \"https://access.redhat.com/security/cve/CVE-2022-23307\",\n            \"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-23307\",\n            \"https://errata.almalinux.org/8/ALSA-2022-0290.html\",\n            \"https://github.com/advisories/GHSA-f7vh-qwp3-x37m\",\n            \"https://linux.oracle.com/cve/CVE-2022-23307.html\",\n            \"https://linux.oracle.com/errata/ELSA-2022-9419.html\",\n            \"https://lists.apache.org/thread/rg4yyc89vs3dw6kpy3r92xop9loywyhh\",\n            \"https://logging.apache.org/log4j/1.2/index.html\",\n            \"https://nvd.nist.gov/vuln/detail/CVE-2022-23307\",\n            \"https://ubuntu.com/security/notices/USN-5998-1\",\n            \"https://www.cve.org/CVERecord?id=CVE-2022-23307\",\n            \"https://www.openwall.com/lists/oss-security/2022/01/18/5\",\n            \"https://www.oracle.com/security-alerts/cpuapr2022.html\",\n            \"https://www.oracle.com/security-alerts/cpujul2022.html\"\n          ],\n          \"PublishedDate\": \"2022-01-18T16:15:00Z\",\n          \"LastModifiedDate\": \"2023-02-24T15:29:00Z\"\n        },\n        {\n          \"VulnerabilityID\": \"CVE-2023-26464\",\n          \"PkgName\": \"log4j:log4j\",\n          \"InstalledVersion\": \"1.2.17\",\n          \"FixedVersion\": \"2.0\",\n          \"Status\": \"fixed\",\n          \"Layer\": {},\n          \"SeveritySource\": \"nvd\",\n          \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2023-26464\",\n          \"DataSource\": {\n            \"ID\": \"glad\",\n            \"Name\": \"GitLab Advisory Database Community\",\n            \"URL\": \"https://gitlab.com/gitlab-org/advisories-community\"\n          },\n          \"Title\": \"DoS via hashmap logging\",\n          \"Description\": \"** UNSUPPORTED WHEN ASSIGNED **\\n\\nWhen using the Chainsaw or SocketAppender components with Log4j 1.x on JRE less than 1.7, an attacker that manages to cause a logging entry involving a specially-crafted (ie, deeply nested) \\nhashmap or hashtable (depending on which logging component is in use) to be processed could exhaust the available memory in the virtual machine and achieve Denial of Service when the object is deserialized.\\n\\nThis issue affects Apache Log4j before 2. Affected users are recommended to update to Log4j 2.x.\\n\\nNOTE: This vulnerability only affects products that are no longer supported by the maintainer.\\n\\n\\n\\n\\n\",\n          \"Severity\": \"UNKNOWN\",\n          \"CweIDs\": [\n            \"CWE-502\"\n          ],\n          \"CVSS\": {\n            \"ghsa\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n              \"V3Score\": 7.5\n            },\n            \"nvd\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n              \"V3Score\": 7.5\n            },\n            \"redhat\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\",\n              \"V3Score\": 7.5\n            }\n          },\n          \"References\": [\n            \"https://access.redhat.com/security/cve/CVE-2023-26464\",\n            \"https://github.com/advisories/GHSA-vp98-w2p3-mv35\",\n            \"https://lists.apache.org/thread/wkx6grrcjkh86crr49p4blc1v1nflj3t\",\n            \"https://nvd.nist.gov/vuln/detail/CVE-2023-26464\",\n            \"https://security.netapp.com/advisory/ntap-20230505-0008/\",\n            \"https://www.cve.org/CVERecord?id=CVE-2023-26464\"\n          ],\n          \"PublishedDate\": \"2023-03-10T14:15:00Z\",\n          \"LastModifiedDate\": \"2023-05-05T20:15:00Z\"\n        },\n        {\n          \"VulnerabilityID\": \"CVE-2020-9488\",\n          \"PkgName\": \"log4j:log4j\",\n          \"InstalledVersion\": \"1.2.17\",\n          \"FixedVersion\": \"2.12.3, 2.13.2\",\n          \"Status\": \"fixed\",\n          \"Layer\": {},\n          \"SeveritySource\": \"nvd\",\n          \"PrimaryURL\": \"https://avd.aquasec.com/nvd/cve-2020-9488\",\n          \"DataSource\": {\n            \"ID\": \"glad\",\n            \"Name\": \"GitLab Advisory Database Community\",\n            \"URL\": \"https://gitlab.com/gitlab-org/advisories-community\"\n          },\n          \"Title\": \"log4j: improper validation of certificate with host mismatch in SMTP appender\",\n          \"Description\": \"Improper validation of certificate with host mismatch in Apache Log4j SMTP appender. This could allow an SMTPS connection to be intercepted by a man-in-the-middle attack which could leak any log messages sent through that appender. Fixed in Apache Log4j 2.12.3 and 2.13.1\",\n          \"Severity\": \"LOW\",\n          \"CweIDs\": [\n            \"CWE-295\"\n          ],\n          \"CVSS\": {\n            \"ghsa\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N\",\n              \"V3Score\": 3.7\n            },\n            \"nvd\": {\n              \"V2Vector\": \"AV:N/AC:M/Au:N/C:P/I:N/A:N\",\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N\",\n              \"V2Score\": 4.3,\n              \"V3Score\": 3.7\n            },\n            \"redhat\": {\n              \"V3Vector\": \"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N\",\n              \"V3Score\": 3.7\n            }\n          },\n          \"References\": [\n            \"https://access.redhat.com/security/cve/CVE-2020-9488\",\n            \"https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-9488\",\n            \"https://gitbox.apache.org/repos/asf?p=logging-log4j2.git;h=6851b5083ef9610bae320bf07e1f24d2aa08851b (release-2.x)\",\n            \"https://gitbox.apache.org/repos/asf?p=logging-log4j2.git;h=fb91a3d71e2f3dadad6fd1beb2ab857f44fe8bbb (master)\",\n            \"https://github.com/advisories/GHSA-vwqq-5vrc-xw9h\",\n            \"https://issues.apache.org/jira/browse/LOG4J2-2819\",\n            \"https://lists.apache.org/thread.html/r0a2699f724156a558afd1abb6c044fb9132caa66dce861b82699722a@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r0df3d7a5acb98c57e64ab9266aa21eeee1d9b399addb96f9cf1cbe05@%3Cdev.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r1fc73f0e16ec2fa249d3ad39a5194afb9cc5afb4c023dc0bab5a5881@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r22a56beb76dd8cf18e24fda9072f1e05990f49d6439662d3782a392f@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r2721aba31a8562639c4b937150897e24f78f747cdbda8641c0f659fe@%3Cusers.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r2f209d271349bafd91537a558a279c08ebcff8fa3e547357d58833e6@%3Cdev.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r33864a0fc171c1c4bf680645ebb6d4f8057899ab294a43e1e4fe9d04@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r393943de452406f0f6f4b3def9f8d3c071f96323c1f6ed1a098f7fe4@%3Ctorque-dev.db.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r3d1d00441c55144a4013adda74b051ae7864128ebcfb6ee9721a2eb3@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r4285398e5585a0456d3d9db021a4fce6e6fcf3ec027dfa13a450ec98@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r45916179811a32cbaa500f972de9098e6ee80ee81c7f134fce83e03a@%3Cissues.flink.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r48bcd06049c1779ef709564544c3d8a32ae6ee5c3b7281a606ac4463@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r48efc7cb5aeb4e1f67aaa06fb4b5479a5635d12f07d0b93fc2d08809@%3Ccommits.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r4d5dc9f3520071338d9ebc26f9f158a43ae28a91923d176b550a807b@%3Cdev.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r4db540cafc5d7232c62e076051ef661d37d345015b2e59b3f81a932f@%3Cdev.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r4ed1f49616a8603832d378cb9d13e7a8b9b27972bb46d946ccd8491f@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r5a68258e5ab12532dc179edae3d6e87037fa3b50ab9d63a90c432507@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r65578f3761a89bc164e8964acd5d913b9f8fd997967b195a89a97ca3@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r7641ee788e1eb1be4bb206a7d15f8a64ec6ef23e5ec6132d5a567695@%3Cnotifications.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r7e5c10534ed06bf805473ac85e8412fe3908a8fa4cabf5027bf11220@%3Cdev.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r7e739f2961753af95e2a3a637828fb88bfca68e5d6b0221d483a9ee5@%3Cnotifications.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8c001b9a95c0bbec06f4457721edd94935a55932e64b82cc5582b846@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r8e96c340004b7898cad3204ea51280ef6e4b553a684e1452bf1b18b1@%3Cjira.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r9776e71e3c67c5d13a91c1eba0dc025b48b802eb7561cc6956d6961c@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/r9a79175c393d14d760a0ae3731b4a873230a16ef321aa9ca48a810cd@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/ra051e07a0eea4943fa104247e69596f094951f51512d42c924e86c75@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/ra632b329b2ae2324fabbad5da204c4ec2e171ff60348ec4ba698fd40@%3Cissues.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rbc45eb0f53fd6242af3e666c2189464f848a851d408289840cecc6e3@%3Ccommits.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rbc7642b9800249553f13457e46b813bea1aec99d2bc9106510e00ff3@%3Ctorque-dev.db.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rc2dbc4633a6eea1fcbce6831876cfa17b73759a98c65326d1896cb1a@%3Ctorque-dev.db.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rc6b81c013618d1de1b5d6b8c1088aaf87b4bacc10c2371f15a566701@%3Cnotifications.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd0e44e8ef71eeaaa3cf3d1b8b41eb25894372e2995ec908ce7624d26@%3Ccommits.pulsar.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd55f65c6822ff235eda435d31488cfbb9aa7055cdf47481ebee777cc@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd5d58088812cf8e677d99b07f73c654014c524c94e7fedbdee047604@%3Ctorque-dev.db.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rd8e87c4d69df335d0ba7d815b63be8bd8a6352f429765c52eb07ddac@%3Cissues.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/re024d86dffa72ad800f2848d0c77ed93f0b78ee808350b477a6ed987@%3Cgitbox.hive.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rec34b1cccf907898e7cb36051ffac3ccf1ea89d0b261a2a3b3fb267f@%3Ccommits.zookeeper.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rf1c2a81a08034c688b8f15cf58a4cfab322d00002ca46d20133bee20@%3Cdev.kafka.apache.org%3E\",\n            \"https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E\",\n            \"https://lists.debian.org/debian-lts-announce/2021/12/msg00017.html\",\n            \"https://nvd.nist.gov/vuln/detail/CVE-2020-9488\",\n            \"https://security.netapp.com/advisory/ntap-20200504-0003/\",\n            \"https://www.cve.org/CVERecord?id=CVE-2020-9488\",\n            \"https://www.debian.org/security/2021/dsa-5020\",\n            \"https://www.openwall.com/lists/oss-security/2020/04/25/1\",\n            \"https://www.oracle.com/security-alerts/cpuApr2021.html\",\n            \"https://www.oracle.com/security-alerts/cpuapr2022.html\",\n            \"https://www.oracle.com/security-alerts/cpujan2021.html\",\n            \"https://www.oracle.com/security-alerts/cpujul2020.html\",\n            \"https://www.oracle.com/security-alerts/cpuoct2020.html\",\n            \"https://www.oracle.com/security-alerts/cpuoct2021.html\"\n          ],\n          \"PublishedDate\": \"2020-04-27T16:15:00Z\",\n          \"LastModifiedDate\": \"2022-05-12T15:00:00Z\"\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "regoservice/testdata/inputs/trivy-operator-input.json",
    "content": "{\n  \"apiVersion\": \"aquasecurity.github.io/v1alpha1\",\n  \"kind\": \"VulnerabilityReport\",\n  \"metadata\": {\n    \"name\": \"replicaset-nginx-6d4cf56db6-nginx\",\n    \"namespace\": \"default\",\n    \"labels\": {\n      \"trivy-operator.container.name\": \"nginx\",\n      \"trivy-operator.resource.kind\": \"ReplicaSet\",\n      \"trivy-operator.resource.name\": \"nginx-6d4cf56db6\",\n      \"trivy-operator.resource.namespace\": \"default\",\n      \"resource-spec-hash\": \"7cb64cb677\"\n    },\n    \"uid\": \"8aa1a7cb-a319-4b93-850d-5a67827dfbbf\",\n    \"ownerReferences\": [\n      {\n        \"apiVersion\": \"apps/v1\",\n        \"blockOwnerDeletion\": false,\n        \"controller\": true,\n        \"kind\": \"ReplicaSet\",\n        \"name\": \"nginx-6d4cf56db6\",\n        \"uid\": \"aa345200-cf24-443a-8f11-ddb438ff8659\"\n      }\n    ]\n  },\n  \"report\": {\n    \"artifact\": {\n      \"repository\": \"library/nginx\",\n      \"tag\": \"1.16\"\n    },\n    \"registry\": {\n      \"server\": \"index.docker.io\"\n    },\n    \"scanner\": {\n      \"name\": \"Trivy\",\n      \"vendor\": \"Aqua Security\",\n      \"version\": \"0.30.0\"\n    },\n    \"summary\": {\n      \"criticalCount\": 2,\n      \"highCount\": 0,\n      \"lowCount\": 0,\n      \"mediumCount\": 0,\n      \"unknownCount\": 0\n    },\n    \"vulnerabilities\": [\n      {\n        \"fixedVersion\": \"0.9.1-2+deb10u1\",\n        \"installedVersion\": \"0.9.1-2\",\n        \"links\": [],\n        \"primaryLink\": \"https://avd.aquasec.com/nvd/cve-2019-20367\",\n        \"resource\": \"libbsd0\",\n        \"score\": 9.1,\n        \"severity\": \"CRITICAL\",\n        \"target\": \"library/nginx:1.21.6\",\n        \"title\": \"\",\n        \"vulnerabilityID\": \"CVE-2019-20367\"\n      },\n      {\n        \"fixedVersion\": \"\",\n        \"installedVersion\": \"0.6.1-2\",\n        \"links\": [],\n        \"primaryLink\": \"https://avd.aquasec.com/nvd/cve-2018-25009\",\n        \"resource\": \"libwebp6\",\n        \"score\": 6.1,\n        \"severity\": \"HIGH\",\n        \"target\": \"library/nginx:1.16\",\n        \"title\": \"libwebp: out-of-bounds read in WebPMuxCreateInternal\",\n        \"vulnerabilityID\": \"CVE-2018-25009\"\n      },\n      {\n        \"fixedVersion\": \"\",\n        \"installedVersion\": \"0.6.1-2\",\n        \"links\": [],\n        \"primaryLink\": \"https://avd.aquasec.com/nvd/cve-2018-25009\",\n        \"resource\": \"libwebp3\",\n        \"score\": 3.2,\n        \"severity\": \"MEDIUM\",\n        \"target\": \"library/nginx:1.16\",\n        \"title\": \"libwebp: out-of-bounds read in WebPMuxCreateInternal\",\n        \"vulnerabilityID\": \"CVE-2018-25010\"\n      },\n      {\n        \"fixedVersion\": \"\",\n        \"installedVersion\": \"0.6.1-2\",\n        \"links\": [],\n        \"primaryLink\": \"https://avd.aquasec.com/nvd/cve-2018-25009\",\n        \"resource\": \"libwebp4\",\n        \"score\": 1.1,\n        \"severity\": \"LOW\",\n        \"target\": \"library/nginx:1.16\",\n        \"title\": \"libwebp: out-of-bounds read in WebPMuxCreateInternal\",\n        \"vulnerabilityID\": \"CVE-2018-25011\"\n      },\n      {\n        \"fixedVersion\": \"\",\n        \"installedVersion\": \"0.6.1-2\",\n        \"links\": [],\n        \"primaryLink\": \"https://avd.aquasec.com/nvd/cve-2018-25009\",\n        \"resource\": \"libwebp5\",\n        \"score\": 0,\n        \"severity\": \"UNKNOWN\",\n        \"target\": \"library/nginx:1.16\",\n        \"title\": \"libwebp: out-of-bounds read in WebPMuxCreateInternal\",\n        \"vulnerabilityID\": \"CVE-2018-25012\"\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "regoservice/testdata/templates/common/common.rego",
    "content": "package postee\nflat_array(a) = o {\n\to:=[item |\n\t\titem:=a[_][_]\n\t]\n}"
  },
  {
    "path": "regoservice/testdata/templates/html-with-complex-pkg.rego",
    "content": "package rego2.html\ntitle:=\"Audit event received\"\nresult:=sprintf(\"Audit event received from %s\", [input.user])\nurl:=\"Audit-registry-received/Audit-image-received\""
  },
  {
    "path": "regoservice/testdata/templates/html.rego",
    "content": "package rego1.html\n\ntitle:=\"Audit event received\"\nresult:=sprintf(\"Audit event received from %s\", [input.user])\nurl:=\"Audit-registry-received/Audit-image-received\""
  },
  {
    "path": "regoservice/testdata/templates/invalid.rego",
    "content": "package rego1.invalid\ndefault input = false"
  },
  {
    "path": "regoservice/testdata/templates/json-without-url.rego",
    "content": "package rego1.json.without.url\ntitle:=\"Audit event received\"\nresult:={\n\t\"assignee\": input.user\n}"
  },
  {
    "path": "regoservice/testdata/templates/json.rego",
    "content": "package rego1.json\ntitle:=\"Audit event received\"\nresult:={\n\t\"assignee\": input.user\n}\nurl:=\"Audit-registry-received/Audit-image-received\""
  },
  {
    "path": "regoservice/testdata/templates/without-any-expression.rego",
    "content": "package rego1.without.any.expression"
  },
  {
    "path": "regoservice/testdata/templates/without-result.rego",
    "content": "package rego1.without.result\nttle:=\"Audit event received\""
  },
  {
    "path": "router/anonymizeSettings_test.go",
    "content": "package router\n\nimport \"testing\"\n\nfunc TestAnonymizeSettings(t *testing.T) {\n\ttests := []struct {\n\t\toriginal *ActionSettings\n\t\texpected *ActionSettings\n\t}{{\n\t\t&ActionSettings{\n\t\t\tUser: \"admin\",\n\t\t},\n\t\t&ActionSettings{\n\t\t\tUser: \"<hidden>\",\n\t\t},\n\t}, {\n\t\t&ActionSettings{\n\t\t\tUser: \"\",\n\t\t},\n\t\t&ActionSettings{\n\t\t\tUser: \"\",\n\t\t},\n\t}, {\n\t\t&ActionSettings{\n\t\t\tPassword: \"secret\",\n\t\t},\n\t\t&ActionSettings{\n\t\t\tPassword: \"<hidden>\",\n\t\t},\n\t}, {\n\t\t&ActionSettings{\n\t\t\tUrl: \"http://localhost\",\n\t\t},\n\t\t&ActionSettings{\n\t\t\tUrl: \"<hidden>\",\n\t\t},\n\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tanonymized := anonymizeSettings(test.original)\n\t\tif anonymized == test.original {\n\t\t\tt.Errorf(\"Anonymized settings weren't cloned\")\n\t\t}\n\t\tif anonymized.User != test.expected.User {\n\t\t\tt.Errorf(\"Settings anonymization is incorrect: expected User %s, got %s\", test.expected.User, anonymized.User)\n\t\t}\n\t\tif anonymized.Password != test.expected.Password {\n\t\t\tt.Errorf(\"Settings anonymization is incorrect: expected Password %s, got %s\", test.expected.Password, anonymized.Password)\n\t\t}\n\t\tif anonymized.Url != test.expected.Url {\n\t\t\tt.Errorf(\"Settings anonymization is incorrect: expected Url %s, got %s\", test.expected.Url, anonymized.Url)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "router/anonymizer.go",
    "content": "package router\n\nimport \"reflect\"\n\nfunc anonymizeSettings(settings *ActionSettings) *ActionSettings {\n\tfieldsToAnonymize := [...]string{\n\t\t\"User\",\n\t\t\"Password\",\n\t\t\"Url\",\n\t\t\"InstanceName\",\n\t}\n\tcopyToAnonymize := *settings\n\n\tfor _, key := range fieldsToAnonymize {\n\n\t\tr := reflect.ValueOf(&copyToAnonymize)\n\t\tv := reflect.Indirect(r).FieldByName(key)\n\t\tprop := v.String()\n\n\t\tif prop != \"\" {\n\t\t\tv.SetString(AnonymizeReplacement)\n\t\t}\n\t}\n\n\treturn &copyToAnonymize\n}\n"
  },
  {
    "path": "router/builders.go",
    "content": "package router\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n)\n\nfunc buildStdoutAction(sourceSettings *ActionSettings) *actions.StdoutAction {\n\treturn &actions.StdoutAction{Name: sourceSettings.Name}\n}\n\nfunc buildSplunkAction(sourceSettings *ActionSettings) *actions.SplunkAction {\n\treturn &actions.SplunkAction{\n\t\tName:       sourceSettings.Name,\n\t\tUrl:        sourceSettings.Url,\n\t\tToken:      sourceSettings.Token,\n\t\tEventLimit: sourceSettings.SizeLimit,\n\t\tTlsVerify:  sourceSettings.TlsVerify,\n\t}\n}\n\nfunc buildWebhookAction(sourceSettings *ActionSettings) *actions.WebhookAction {\n\treturn &actions.WebhookAction{\n\t\tName:    sourceSettings.Name,\n\t\tUrl:     sourceSettings.Url,\n\t\tTimeout: sourceSettings.Timeout,\n\t}\n}\n\nfunc buildTeamsAction(sourceSettings *ActionSettings, aquaServer string) *actions.TeamsAction {\n\treturn &actions.TeamsAction{\n\t\tName:       sourceSettings.Name,\n\t\tAquaServer: aquaServer,\n\t\tWebhook:    sourceSettings.Url,\n\t}\n}\n\nfunc buildServiceNow(sourceSettings *ActionSettings) *actions.ServiceNowAction {\n\tserviceNow := &actions.ServiceNowAction{\n\t\tName:     sourceSettings.Name,\n\t\tUser:     sourceSettings.User,\n\t\tPassword: sourceSettings.Password,\n\t\tTable:    sourceSettings.BoardName,\n\t\tInstance: sourceSettings.InstanceName,\n\t}\n\tif len(serviceNow.Table) == 0 {\n\t\tserviceNow.Table = ServiceNowTableDefault\n\t}\n\treturn serviceNow\n}\n\nfunc buildSlackAction(sourceSettings *ActionSettings, aqua string) *actions.SlackAction {\n\treturn &actions.SlackAction{\n\t\tName:       sourceSettings.Name,\n\t\tAquaServer: aqua,\n\t\tUrl:        sourceSettings.Url,\n\t}\n}\n\nfunc buildEmailAction(sourceSettings *ActionSettings) *actions.EmailAction {\n\treturn &actions.EmailAction{\n\t\tName:           sourceSettings.Name,\n\t\tUser:           sourceSettings.User,\n\t\tPassword:       sourceSettings.Password,\n\t\tHost:           sourceSettings.Host,\n\t\tPort:           sourceSettings.Port,\n\t\tSender:         sourceSettings.Sender,\n\t\tRecipients:     sourceSettings.Recipients,\n\t\tClientHostName: sourceSettings.ClientHostName,\n\t\tUseMX:          sourceSettings.UseMX,\n\t}\n}\nfunc buildNexusIqAction(sourceSettings *ActionSettings) *actions.NexusIqAction {\n\treturn &actions.NexusIqAction{\n\t\tName:           sourceSettings.Name,\n\t\tUrl:            sourceSettings.Url,\n\t\tUser:           sourceSettings.User,\n\t\tPassword:       sourceSettings.Password,\n\t\tOrganizationId: sourceSettings.OrganizationId,\n\t}\n}\n\nfunc buildDependencyTrackAction(sourceSettings *ActionSettings) *actions.DependencyTrackAction {\n\treturn &actions.DependencyTrackAction{\n\t\tName:   sourceSettings.Name,\n\t\tUrl:    sourceSettings.Url,\n\t\tAPIKey: sourceSettings.DependencyTrackAPIKey,\n\t}\n}\n\nfunc buildOpsGenieAction(sourceSettings *ActionSettings) *actions.OpsGenieAction {\n\treturn &actions.OpsGenieAction{\n\t\tName:           sourceSettings.Name,\n\t\tUser:           sourceSettings.User,\n\t\tAPIKey:         sourceSettings.Token,\n\t\tResponders:     sourceSettings.Assignee,\n\t\tVisibleTo:      sourceSettings.Recipients,\n\t\tPrioritySource: sourceSettings.Priority,\n\t\tTags:           sourceSettings.Tags,\n\t\tAlias:          sourceSettings.Alias,\n\t\tEntity:         sourceSettings.Entity,\n\t}\n}\n\nfunc buildJiraAction(sourceSettings *ActionSettings) *actions.JiraAPI {\n\tjiraApi := &actions.JiraAPI{\n\t\tName:            sourceSettings.Name,\n\t\tUrl:             sourceSettings.Url,\n\t\tUser:            sourceSettings.User,\n\t\tPassword:        sourceSettings.Password,\n\t\tToken:           sourceSettings.Token,\n\t\tTlsVerify:       sourceSettings.TlsVerify,\n\t\tIssuetype:       sourceSettings.IssueType,\n\t\tProjectKey:      strings.ToUpper(sourceSettings.ProjectKey),\n\t\tPriority:        sourceSettings.Priority,\n\t\tAssignee:        sourceSettings.Assignee,\n\t\tFixVersions:     sourceSettings.FixVersions,\n\t\tAffectsVersions: sourceSettings.AffectsVersions,\n\t\tLabels:          sourceSettings.Labels,\n\t\tUnknowns:        sourceSettings.Unknowns,\n\t\tSprintName:      sourceSettings.Sprint,\n\t\tSprintId:        actions.NotConfiguredSprintId,\n\t\tBoardName:       sourceSettings.BoardName,\n\t}\n\n\tif len(jiraApi.Assignee) == 0 {\n\t\tjiraApi.Assignee = []string{jiraApi.User}\n\t}\n\treturn jiraApi\n}\n\nfunc buildExecAction(sourceSettings *ActionSettings) (*actions.ExecClient, error) {\n\tif len(sourceSettings.InputFile) <= 0 && len(sourceSettings.ExecScript) <= 0 {\n\t\treturn nil, fmt.Errorf(\"exec action requires either input-file or exec-script to be set\")\n\t}\n\n\tif len(sourceSettings.InputFile) > 0 && len(sourceSettings.ExecScript) > 0 {\n\t\treturn nil, fmt.Errorf(\"exec action only takes either input-file or exec-script, not both\")\n\t}\n\n\tec := &actions.ExecClient{\n\t\tName: sourceSettings.Name,\n\t\tEnv:  sourceSettings.Env,\n\t}\n\n\tif len(sourceSettings.InputFile) > 0 {\n\t\tec.InputFile = sourceSettings.InputFile\n\t}\n\n\tif len(sourceSettings.ExecScript) > 0 {\n\t\tec.ExecScript = sourceSettings.ExecScript\n\t}\n\n\treturn ec, nil\n}\n\nfunc buildHTTPAction(sourceSettings *ActionSettings) (*actions.HTTPClient, error) {\n\tif len(sourceSettings.Method) <= 0 {\n\t\treturn nil, fmt.Errorf(\"http action requires a method to be specified\")\n\t}\n\n\tif len(sourceSettings.BodyFile) > 0 && len(sourceSettings.BodyContent) > 0 {\n\t\treturn nil, fmt.Errorf(\"http action requires only supports body-file or body-content, not both\")\n\t}\n\n\tvar duration time.Duration\n\tif len(sourceSettings.Timeout) > 0 {\n\t\tvar err error\n\t\tduration, err = time.ParseDuration(sourceSettings.Timeout)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid duration specified: %w\", err)\n\t\t}\n\t} else {\n\t\tduration = time.Second * 5\n\t}\n\n\treqUrl, err := url.Parse(sourceSettings.Url)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error building HTTP url: %w\", err)\n\t}\n\n\thc := &actions.HTTPClient{\n\t\tName:    sourceSettings.Name,\n\t\tClient:  http.Client{Timeout: duration},\n\t\tURL:     reqUrl,\n\t\tMethod:  strings.ToUpper(sourceSettings.Method),\n\t\tHeaders: sourceSettings.Headers,\n\t}\n\n\tif len(sourceSettings.BodyFile) > 0 {\n\t\thc.BodyFile = sourceSettings.BodyFile\n\t}\n\tif len(sourceSettings.BodyContent) > 0 {\n\t\thc.BodyContent = sourceSettings.BodyContent\n\t}\n\n\treturn hc, nil\n}\n\nfunc buildKubernetesAction(sourceSettings *ActionSettings) (*actions.KubernetesClient, error) {\n\tif !actions.IsK8s() {\n\t\tif sourceSettings.KubeConfigFile == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"kubernetes config file needs to be set in config yaml\")\n\t\t}\n\t}\n\n\tif sourceSettings.KubeNamespace == \"\" {\n\t\treturn nil, fmt.Errorf(\"kubernetes namespace needs to be set in config yaml\")\n\t}\n\n\treturn &actions.KubernetesClient{\n\t\tName:              sourceSettings.Name,\n\t\tKubeNamespace:     sourceSettings.KubeNamespace,\n\t\tKubeConfigFile:    sourceSettings.KubeConfigFile,\n\t\tKubeLabelSelector: sourceSettings.KubeLabelSelector,\n\t\tKubeActions:       sourceSettings.KubeActions,\n\t}, nil\n}\n\nfunc buildDockerAction(sourceSettings *ActionSettings) (*actions.DockerClient, error) {\n\tif len(sourceSettings.DockerImageName) < 0 {\n\t\treturn nil, fmt.Errorf(\"docker action requires an image name\")\n\t}\n\n\treturn &actions.DockerClient{\n\t\tName:      sourceSettings.Name,\n\t\tImageName: sourceSettings.DockerImageName,\n\t\tCmd:       sourceSettings.DockerCmd,\n\t\tVolumes:   sourceSettings.DockerVolumes,\n\t\tEnv:       sourceSettings.DockerEnv,\n\t\tNetwork:   sourceSettings.DockerNetwork,\n\t}, nil\n}\n\nfunc buildAWSSecurityHubAction(sourceSettings *ActionSettings) (*actions.AWSSecurityHubClient, error) {\n\treturn &actions.AWSSecurityHubClient{Name: sourceSettings.Name}, nil\n}\n\nfunc buildPagerdutyAction(sourceSettings *ActionSettings) (*actions.PagerdutyClient, error) {\n\treturn &actions.PagerdutyClient{\n\t\tName:       sourceSettings.Name,\n\t\tAuthToken:  sourceSettings.PagerdutyAuthToken,\n\t\tRoutingKey: sourceSettings.PagerdutyRoutingKey,\n\t}, nil\n}\n"
  },
  {
    "path": "router/goldens/kube-config.sample",
    "content": "apiVersion: v1\nclusters:\n- cluster:\n    server: https://kubernetes.docker.internal:6443\n  name: foo\ncontexts:\n- context:\n    cluster: foo\n    user: foo\n  name: foo\ncurrent-context: foo\nkind: Config\npreferences: {}"
  },
  {
    "path": "router/goldens/sample.cfg",
    "content": "name: Postee Controller Runner Demo\n\naqua-server:            #  URL of Aqua Server for links. E.g. https://myserver.aquasec.com\nmax-db-size: 1000MB       #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\ndb-verify-interval: 1   #  How often to check the DB size. By default, Postee checks every 1 hour\n\n# Routes are used to define how to handle an incoming message\nroutes:\n- name: stdout\n  actions: [ stdout ]\n  template: raw-json\n\n- name: controller-only-route\n  input: contains(input.image, \"alpine\")\n  actions: [my-http-post-from-controller]\n  template: raw-json\n\n- name: runner-only-route\n  input: contains(input.SigMetadata.ID, \"TRC-1\")\n  serialize-actions: true\n  actions: [my-exec-from-runner, my-http-post-from-runner]\n  template: raw-json\n\n- name: controller-runner-route\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  serialize-actions: true     # Cannot be strictly guaranteed as executions happen independently on runner/controller\n  actions: [my-exec-from-runner, my-http-post-from-runner, my-http-post-from-controller]\n  template: raw-json\n\n# Templates are used to format a message\ntemplates:\n- name: raw-json                        # route message \"As Is\" to external webhook\n  rego-package: postee.rawmessage.json\n\n# Outputs are target services that should consume the messages\nactions:\n- name: stdout\n  type: stdout\n  enable: true\n\n- name: my-http-post-from-controller\n  type: http\n  enable: true\n  url: \"https://webhook.site/<uuid>\"       # Required. URL of the HTTP Request\n  method: POST                          # Required. Method to use. CONNECT is not supported at this time\n  headers:                              # Optional. Headers to pass in for the request.\n    \"Foo\": [ \"bar\" ]\n  timeout: 10s                          # Optional. Timeout value in XX(s,m,h)\n  body-content: |                       # Optional. Body inline content of the HTTP request\n    This is an example of a inline body\n    Input Image: event.input.image\n\n- name: my-exec-from-runner\n  runs-on: \"postee-runner-1\"\n  type: exec\n  enable: true\n  env: [\"MY_ENV_VAR=foo_bar_baz\", \"MY_KEY=secret\"]     # Optional. Any environment variables to pass in\n  exec-script: |                                       # Specify the script to run\n    #!/bin/sh\n    echo $POSTEE_EVENT\n    echo \"this is hello from postee\"\n\n- name: my-http-post-from-runner\n  runs-on: \"postee-runner-1\"\n  type: http\n  enable: true\n  url: \"https://webhook.site/<uuid>\"       # Required. URL of the HTTP Request\n  method: POST                          # Required. Method to use. CONNECT is not supported at this time\n  body-content: |                       # Optional. Body inline content of the HTTP request\n    This is an another example of a inline body\n    Event ID: event.input.SigMetadata.ID\n"
  },
  {
    "path": "router/goldens/test.txt",
    "content": "foo bar baz"
  },
  {
    "path": "router/initoutputs_test.go",
    "content": "package router\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBuildAndInitOtpt(t *testing.T) {\n\ttests := []struct {\n\t\tcaseDesc            string\n\t\tactionSettings      ActionSettings\n\t\texpctdProps         map[string]interface{}\n\t\tshouldFail          bool\n\t\texpectedActionClass string\n\t}{\n\t\t{\n\t\t\t\"Default Stdout Action\",\n\t\t\tActionSettings{\n\t\t\t\tName:   \"stdout\",\n\t\t\t\tType:   \"stdout\",\n\t\t\t\tEnable: true,\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Name\": \"stdout\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.StdoutAction\",\n\t\t},\n\t\t{\n\t\t\t\"Simple Slack\",\n\t\t\tActionSettings{\n\t\t\t\tName:   \"my-slack\",\n\t\t\t\tType:   \"slack\",\n\t\t\t\tEnable: true,\n\t\t\t\tUrl:    \"https://hooks.slack.com/services/TT/BBB/WWWW\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Url\":  \"https://hooks.slack.com/services/TT/BBB/WWWW\",\n\t\t\t\t\"Name\": \"my-slack\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.SlackAction\",\n\t\t},\n\t\t{\n\t\t\t\"Simple Email action\",\n\t\t\tActionSettings{\n\t\t\t\tUser:       \"EmailUser\",\n\t\t\t\tPassword:   \"pAsSw0rD\",\n\t\t\t\tHost:       \"smtp.gmail.com\",\n\t\t\t\tName:       \"my-email\",\n\t\t\t\tType:       \"email\",\n\t\t\t\tPort:       587,\n\t\t\t\tSender:     \"google@gmail.com\",\n\t\t\t\tRecipients: []string{\"r1@gmail.com\"},\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"User\":       \"EmailUser\",\n\t\t\t\t\"Password\":   \"pAsSw0rD\",\n\t\t\t\t\"Host\":       \"smtp.gmail.com\",\n\t\t\t\t\"Port\":       587,\n\t\t\t\t\"Sender\":     \"google@gmail.com\",\n\t\t\t\t\"Recipients\": []string{\"r1@gmail.com\"},\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.EmailAction\",\n\t\t},\n\t\t{\n\t\t\t\"Simple Nexus IQ action\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:            \"http://localhost:8070\",\n\t\t\t\tUser:           \"admin\",\n\t\t\t\tPassword:       \"admin123\",\n\t\t\t\tName:           \"my-nexus\",\n\t\t\t\tType:           \"nexusIq\",\n\t\t\t\tOrganizationId: \"222de33e8005408a844c12eab952c9b0\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Url\":            \"http://localhost:8070\",\n\t\t\t\t\"User\":           \"admin\",\n\t\t\t\t\"Password\":       \"admin123\",\n\t\t\t\t\"OrganizationId\": \"222de33e8005408a844c12eab952c9b0\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.NexusIqAction\",\n\t\t},\n\t\t{\n\t\t\t\"Simple Dependency Track action\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:                   \"http://localhost:8080\",\n\t\t\t\tName:                  \"my-dependencytrack\",\n\t\t\t\tType:                  \"dependencytrack\",\n\t\t\t\tDependencyTrackAPIKey: \"api-key\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Url\":    \"http://localhost:8080\",\n\t\t\t\t\"APIKey\": \"api-key\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.DependencyTrackAction\",\n\t\t},\n\t\t{\n\t\t\t\"Simple Jira action\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:        \"localhost:2990\",\n\t\t\t\tUser:       \"admin\",\n\t\t\t\tPassword:   \"admin\",\n\t\t\t\tName:       \"my-jira\",\n\t\t\t\tType:       \"jira\",\n\t\t\t\tProjectKey: \"PK\",\n\t\t\t\tIssueType:  \"IssueType\",\n\t\t\t\tPriority:   \"Priority\",\n\t\t\t\tAssignee:   []string{\"Assignee\"},\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Url\":        \"localhost:2990\",\n\t\t\t\t\"User\":       \"admin\",\n\t\t\t\t\"Password\":   \"admin\",\n\t\t\t\t\"ProjectKey\": \"PK\",\n\t\t\t\t\"Issuetype\":  \"IssueType\",\n\t\t\t\t\"Priority\":   \"Priority\",\n\t\t\t\t\"Assignee\":   []string{\"Assignee\"},\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.JiraAPI\",\n\t\t},\n\t\t{\n\t\t\t\"Jira action without credentials\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:        \"localhost:2990\",\n\t\t\t\tName:       \"my-jira\",\n\t\t\t\tType:       \"jira\",\n\t\t\t\tProjectKey: \"PK\",\n\t\t\t\tIssueType:  \"IssueType\",\n\t\t\t\tPriority:   \"Priority\",\n\t\t\t\tAssignee:   []string{\"Assignee\"},\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\n\t\t\t\"Jira action without password\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:        \"localhost:2990\",\n\t\t\t\tUser:       \"admin\",\n\t\t\t\tName:       \"my-jira\",\n\t\t\t\tType:       \"jira\",\n\t\t\t\tProjectKey: \"PK\",\n\t\t\t\tIssueType:  \"IssueType\",\n\t\t\t\tPriority:   \"Priority\",\n\t\t\t\tAssignee:   []string{\"Assignee\"},\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\n\t\t\t\"Jira action with missed type\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:        \"localhost:2990\",\n\t\t\t\tUser:       \"admin\",\n\t\t\t\tName:       \"my-jira\",\n\t\t\t\tProjectKey: \"PK\",\n\t\t\t\tIssueType:  \"IssueType\",\n\t\t\t\tPriority:   \"Priority\",\n\t\t\t\tAssignee:   []string{\"Assignee\"},\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\n\t\t\t\"Jira Action with some default values\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:        \"localhost:2990\",\n\t\t\t\tName:       \"my-jira-with-defaults\",\n\t\t\t\tType:       \"jira\",\n\t\t\t\tUser:       \"admin\",\n\t\t\t\tPassword:   \"admin\",\n\t\t\t\tProjectKey: \"PK\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Url\":        \"localhost:2990\",\n\t\t\t\t\"User\":       \"admin\",\n\t\t\t\t\"Password\":   \"admin\",\n\t\t\t\t\"ProjectKey\": \"PK\",\n\t\t\t\t\"Assignee\":   []string{\"admin\"},\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.JiraAPI\",\n\t\t},\n\t\t{\n\t\t\t\"Simple webhook action\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:  \"http://localhost:8080\",\n\t\t\t\tName: \"my-webhook\",\n\t\t\t\tType: \"webhook\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Url\": \"http://localhost:8080\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.WebhookAction\",\n\t\t},\n\t\t{\n\t\t\t\"Simple ServiceNow action\",\n\t\t\tActionSettings{\n\t\t\t\tName:         \"my-servicenow\",\n\t\t\t\tType:         \"serviceNow\",\n\t\t\t\tUser:         \"admin\",\n\t\t\t\tPassword:     \"secret\",\n\t\t\t\tInstanceName: \"dev108148\",\n\t\t\t\tBoardName:    \"incindent\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"User\":     \"admin\",\n\t\t\t\t\"Password\": \"secret\",\n\t\t\t\t\"Instance\": \"dev108148\",\n\t\t\t\t\"Table\":    \"incindent\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.ServiceNowAction\",\n\t\t},\n\t\t{\n\t\t\t\"ServiceNow action without BoardName\",\n\t\t\tActionSettings{\n\t\t\t\tName:         \"my-servicenow\",\n\t\t\t\tType:         \"serviceNow\",\n\t\t\t\tUser:         \"admin\",\n\t\t\t\tPassword:     \"secret\",\n\t\t\t\tInstanceName: \"dev108148\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"User\":     \"admin\",\n\t\t\t\t\"Password\": \"secret\",\n\t\t\t\t\"Instance\": \"dev108148\",\n\t\t\t\t\"Table\":    ServiceNowTableDefault,\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.ServiceNowAction\",\n\t\t},\n\t\t{\n\t\t\t\"Simple Teams action\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:  \"https://outlook.office.com/webhook/ABCD\",\n\t\t\t\tName: \"my-teams\",\n\t\t\t\tType: \"teams\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Webhook\": \"https://outlook.office.com/webhook/ABCD\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.TeamsAction\",\n\t\t},\n\t\t{\n\t\t\t\"Simple Splunk action\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:   \"http://localhost:8088\",\n\t\t\t\tName:  \"my-splunk\",\n\t\t\t\tType:  \"splunk\",\n\t\t\t\tToken: \"test_token_for_splunk\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Url\":   \"http://localhost:8088\",\n\t\t\t\t\"Name\":  \"my-splunk\",\n\t\t\t\t\"Token\": \"test_token_for_splunk\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.SplunkAction\",\n\t\t},\n\t\t{\n\t\t\t\"HTTP Action action, with a timeout & body file specified\",\n\t\t\tActionSettings{\n\t\t\t\tMethod:   \"POST\",\n\t\t\t\tTimeout:  \"10s\",\n\t\t\t\tUrl:      \"https://foo.bar.com\",\n\t\t\t\tName:     \"my-http-action\",\n\t\t\t\tType:     \"http\",\n\t\t\t\tBodyFile: \"goldens/test.txt\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Name\":     \"my-http-action\",\n\t\t\t\t\"Method\":   \"POST\",\n\t\t\t\t\"BodyFile\": \"goldens/test.txt\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.HTTPClient\",\n\t\t},\n\t\t{\n\t\t\t\"HTTP Action action, with a timeout & body content specified\",\n\t\t\tActionSettings{\n\t\t\t\tMethod:      \"POST\",\n\t\t\t\tTimeout:     \"10s\",\n\t\t\t\tUrl:         \"https://foo.bar.com\",\n\t\t\t\tName:        \"my-http-action\",\n\t\t\t\tType:        \"http\",\n\t\t\t\tBodyContent: \"foo bar baz body\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Name\":        \"my-http-action\",\n\t\t\t\t\"Method\":      \"POST\",\n\t\t\t\t\"BodyContent\": \"foo bar baz body\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.HTTPClient\",\n\t\t},\n\t\t{\n\t\t\t\"HTTP Action action, with a timeout & both body content and file specified\",\n\t\t\tActionSettings{\n\t\t\t\tMethod:      \"POST\",\n\t\t\t\tTimeout:     \"10s\",\n\t\t\t\tUrl:         \"https://foo.bar.com\",\n\t\t\t\tName:        \"my-http-action\",\n\t\t\t\tType:        \"http\",\n\t\t\t\tBodyFile:    \"goldens/test.txt\",\n\t\t\t\tBodyContent: \"foo bar baz body\",\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\n\t\t\t\"HTTP Action action, with no method specified\",\n\t\t\tActionSettings{\n\t\t\t\tUrl:  \"https://foo.bar.com\",\n\t\t\t\tName: \"my-http-action\",\n\t\t\t\tType: \"http\",\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\n\t\t\t\"HTTP Action action, with invalid url specified\",\n\t\t\tActionSettings{\n\t\t\t\tMethod: \"get\",\n\t\t\t\tUrl:    \"http://[fe80::1%en0]/\",\n\t\t\t\tName:   \"my-http-action\",\n\t\t\t\tType:   \"http\",\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\n\t\t\t\"HTTP Action action, with a invalid timeout\",\n\t\t\tActionSettings{\n\t\t\t\tMethod:  \"GET\",\n\t\t\t\tTimeout: \"ten seconds\",\n\t\t\t\tType:    \"http\",\n\t\t\t},\n\t\t\tmap[string]interface{}{}, true,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\"Exec Action action, with input-file config\",\n\t\t\tActionSettings{\n\t\t\t\tName:      \"my-exec-action\",\n\t\t\t\tEnv:       []string{\"foo=bar\"},\n\t\t\t\tInputFile: \"goldens/test.txt\",\n\t\t\t\tType:      \"exec\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Name\":      \"my-exec-action\",\n\t\t\t\t\"InputFile\": \"goldens/test.txt\",\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.ExecClient\",\n\t\t},\n\t\t{\"Exec Action action, with exec-script config\",\n\t\t\tActionSettings{\n\t\t\t\tName: \"my-exec-action\",\n\t\t\t\tEnv:  []string{\"foo=bar\"},\n\t\t\t\tExecScript: `#!/bin/sh\necho \"foo bar\"`,\n\t\t\t\tType: \"exec\",\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Name\": \"my-exec-action\",\n\t\t\t\t\"ExecScript\": `#!/bin/sh\necho \"foo bar\"`,\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.ExecClient\",\n\t\t},\n\t\t{\"Exec Action action, with invalid config (both file and script)\",\n\t\t\tActionSettings{\n\t\t\t\tName:      \"my-exec-action\",\n\t\t\t\tEnv:       []string{\"foo=bar\"},\n\t\t\t\tInputFile: \"goldens/test.txt\",\n\t\t\t\tExecScript: `#!/bin/sh\necho \"foo bar\"`,\n\t\t\t\tType: \"exec\",\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\"Exec Action action, with invalid config (no file nor script)\",\n\t\t\tActionSettings{\n\t\t\t\tName: \"my-exec-output\",\n\t\t\t\tEnv:  []string{\"foo=bar\"},\n\t\t\t\tType: \"exec\",\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\n\t\t\t\"Kubernetes Action, happy path\",\n\t\t\tActionSettings{\n\t\t\t\tName:              \"my-k8s-output\",\n\t\t\t\tType:              \"kubernetes\",\n\t\t\t\tKubeNamespace:     \"default\",\n\t\t\t\tKubeConfigFile:    \"goldens/kube-config.sample\",\n\t\t\t\tKubeLabelSelector: \"app=foobar\",\n\t\t\t\tKubeActions: map[string]map[string]string{\n\t\t\t\t\t\"labels\":      {\"foo-label\": \"bar-value\"},\n\t\t\t\t\t\"annotations\": {\"foo-annotation\": \"bar-value\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmap[string]interface{}{\n\t\t\t\t\"Name\":              \"my-k8s-output\",\n\t\t\t\t\"KubeNamespace\":     \"default\",\n\t\t\t\t\"KubeConfigFile\":    \"goldens/kube-config.sample\",\n\t\t\t\t\"KubeLabelSelector\": \"app=foobar\",\n\t\t\t\t\"KubeActions\": map[string]map[string]string{\n\t\t\t\t\t\"labels\":      {\"foo-label\": \"bar-value\"},\n\t\t\t\t\t\"annotations\": {\"foo-annotation\": \"bar-value\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tfalse,\n\t\t\t\"*actions.KubernetesClient\",\n\t\t},\n\t\t{\n\t\t\t\"Kubernetes Action, sad path, no kube-config\",\n\t\t\tActionSettings{\n\t\t\t\tName:              \"my-k8s-output\",\n\t\t\t\tType:              \"kubernetes\",\n\t\t\t\tKubeNamespace:     \"default\",\n\t\t\t\tKubeLabelSelector: \"app=foobar\",\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t\t{\n\t\t\t\"Kubernetes Action, sad path, no kube namespace\",\n\t\t\tActionSettings{\n\t\t\t\tName:              \"my-k8s-output\",\n\t\t\t\tType:              \"kubernetes\",\n\t\t\t\tKubeConfigFile:    \"goldens/kube-config.sample\",\n\t\t\t\tKubeLabelSelector: \"app=foobar\",\n\t\t\t},\n\t\t\tmap[string]interface{}{},\n\t\t\ttrue,\n\t\t\t\"<nil>\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.caseDesc, func(t *testing.T) {\n\t\t\to := BuildAndInitOtpt(&test.actionSettings, \"\")\n\t\t\tif test.shouldFail && o != nil {\n\t\t\t\tt.Fatalf(\"No output expected for %s test case but was %s\", test.caseDesc, o)\n\t\t\t} else if !test.shouldFail && o == nil {\n\t\t\t\tt.Fatalf(\"Not expected output returned for %s test case\", test.caseDesc)\n\t\t\t}\n\t\t\tactualActionCls := fmt.Sprintf(\"%T\", o)\n\t\t\tif actualActionCls != test.expectedActionClass {\n\t\t\t\tt.Errorf(\"[%s] Incorrect output type, expected %s, got %s\", test.caseDesc, test.expectedActionClass, actualActionCls)\n\t\t\t}\n\n\t\t\tfor key, prop := range test.expctdProps {\n\t\t\t\t//t.Logf(\"key %s\\n\", key)\n\t\t\t\tr := reflect.ValueOf(o)\n\t\t\t\tv := reflect.Indirect(r).FieldByName(key)\n\t\t\t\tif !v.IsValid() {\n\t\t\t\t\tt.Errorf(\"Property %s is not found\", key)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tmbStringSlice, ok := prop.([]string)\n\t\t\t\tif ok {\n\t\t\t\t\tvSlice, ok := v.Interface().([]string)\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tt.Errorf(\"Invalid type of property %s, expected []string, got %T\", key, v.Interface())\n\t\t\t\t\t}\n\n\t\t\t\t\tif len(mbStringSlice) == len(vSlice) {\n\t\t\t\t\t\tfor i := range mbStringSlice {\n\t\t\t\t\t\t\tif mbStringSlice[i] != vSlice[i] {\n\t\t\t\t\t\t\t\tt.Errorf(\"Invalid property %s, expected: %q, got: %q\",\n\t\t\t\t\t\t\t\t\tkey, mbStringSlice[i], vSlice[i])\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tt.Errorf(\"Wrong size of %s, expected: %d, got: %d\", key, len(mbStringSlice), len(vSlice))\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\t\t\t\t\tassert.EqualValues(t, prop, v.Interface())\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "router/inittemplate_test.go",
    "content": "package router\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"os\"\n\t\"testing\"\n)\n\nvar (\n\tregoRule = \"package postee.slack\"\n)\n\nfunc TestInitTemplate(t *testing.T) {\n\tsavedGetHttpClient := getHttpClient\n\tgetHttpClient = getMockedHttpClient\n\tdefaultRegoFolder := \"rego-templates\"\n\tcommonRegoFolder := defaultRegoFolder + \"/common\"\n\ttestRego := defaultRegoFolder + \"/rego1.rego\"\n\terr := os.Mkdir(defaultRegoFolder, 0777)\n\tif err != nil {\n\t\tt.Fatalf(\"Can't create rego folder: %v\", err)\n\t}\n\terr = os.Mkdir(commonRegoFolder, 0777)\n\tif err != nil {\n\t\tt.Fatalf(\"Can't create rego folder: %v\", err)\n\t}\n\n\terr = ioutil.WriteFile(testRego, []byte(regoRule), 0644)\n\n\tif err != nil {\n\t\tt.Fatalf(\"Can't write rego: %v\", err)\n\t}\n\n\tdefer func() {\n\t\tos.Remove(testRego)\n\t\tos.Remove(commonRegoFolder)\n\t\tos.Remove(defaultRegoFolder)\n\t\tgetHttpClient = savedGetHttpClient\n\t}()\n\n\ttests := []struct {\n\t\ttemplate          *Template\n\t\tcaseDesc          string\n\t\texpectedCls       string\n\t\tshouldReturnError bool\n\t}{\n\t\t{\n\t\t\ttemplate: &Template{\n\t\t\t\tName:               \"legacy-html\",\n\t\t\t\tLegacyScanRenderer: \"html\",\n\t\t\t},\n\t\t\tcaseDesc:    \"Legacy mode test\",\n\t\t\texpectedCls: \"*formatting.legacyScnEvaluator\",\n\t\t},\n\t\t{\n\t\t\ttemplate: &Template{\n\t\t\t\tName:        \"built-in\",\n\t\t\t\tRegoPackage: \"postee.slack\",\n\t\t\t},\n\t\t\tcaseDesc:    \"Built-in rego package\",\n\t\t\texpectedCls: \"*regoservice.regoEvaluator\",\n\t\t},\n\t\t{\n\t\t\ttemplate: &Template{\n\t\t\t\tName: \"from-url\",\n\t\t\t\tUrl:  \"http://localhost/slack.rego\",\n\t\t\t},\n\t\t\tcaseDesc:    \"Loading rego from url\",\n\t\t\texpectedCls: \"*regoservice.regoEvaluator\",\n\t\t},\n\t\t{\n\t\t\ttemplate: &Template{\n\t\t\t\tName: \"not-found\",\n\t\t\t\tUrl:  \"http://localhost/wrong.rego\",\n\t\t\t},\n\t\t\tcaseDesc:          \"Loading rego from not existing url\",\n\t\t\texpectedCls:       \"*regoservice.regoEvaluator\",\n\t\t\tshouldReturnError: true,\n\t\t},\n\t\t{\n\t\t\ttemplate: &Template{\n\t\t\t\tName: \"from-invalid-url\",\n\t\t\t\tUrl:  \"invalid-url\",\n\t\t\t},\n\t\t\tcaseDesc:          \"Loading rego from invalid url\",\n\t\t\texpectedCls:       \"*regoservice.regoEvaluator\",\n\t\t\tshouldReturnError: true,\n\t\t},\n\t\t{\n\t\t\ttemplate: &Template{\n\t\t\t\tName: \"inline\",\n\t\t\t\tBody: \"package postee.inline\",\n\t\t\t},\n\t\t\tcaseDesc:    \"Loading rego from yaml config\",\n\t\t\texpectedCls: \"*regoservice.regoEvaluator\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tdoInitTemplate(t, test.caseDesc, test.template, test.expectedCls, test.shouldReturnError)\n\t}\n\n}\nfunc doInitTemplate(t *testing.T, caseDesc string, template *Template, expectedCls string, shouldReturnError bool) {\n\tdemoCtx := Instance()\n\terr := demoCtx.initTemplate(template)\n\tif err != nil && !shouldReturnError {\n\t\tt.Fatalf(\"[%s] Unexpected error: %v\", caseDesc, err)\n\t}\n\tif err == nil && shouldReturnError {\n\t\tt.Fatalf(\"Test case [%s] should return an error\", caseDesc)\n\t}\n\tif shouldReturnError {\n\t\treturn\n\t}\n\n\tinitialized, ok := demoCtx.templates[template.Name]\n\tif !ok {\n\t\tt.Fatalf(\"[%s] template %s is not initialized\", caseDesc, template.Name)\n\t}\n\tactualCls := fmt.Sprintf(\"%T\", initialized)\n\tif actualCls != expectedCls {\n\t\tt.Errorf(\"[%s] Unexpected type of input evaluator. Expected %s, got %s \\n\", caseDesc, expectedCls, actualCls)\n\t}\n}\n\n//stuff for mocking http requests goes below\n\nfunc getMockedHttpClient() *http.Client {\n\treturn NewTestClient(responseWithRego)\n}\n\n// RoundTripFunc\ntype RoundTripFunc func(req *http.Request) (*http.Response, error)\n\n// RoundTrip\nfunc (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { //this is kind of wrapper where original function is used in interface implementation\n\treturn f(req)\n}\n\n//NewTestClient returns *http.Client with Transport replaced to avoid making real calls\nfunc NewTestClient(fn RoundTripFunc) *http.Client {\n\treturn &http.Client{\n\t\tTransport: RoundTripFunc(fn),\n\t}\n}\nfunc responseWithRego(req *http.Request) (*http.Response, error) {\n\tif req.URL.String() == \"http://localhost/wrong.rego\" {\n\t\tfmt.Printf(\"response status is %d\\n\", 404)\n\t\treturn newTestResponse(404, \"<html>not found</html>\"), nil\n\t} else if req.URL.String() == \"invalid-url\" {\n\t\treturn nil, errors.New(\"invalid url\")\n\t} else {\n\t\tfmt.Printf(\"response status is %d\\n\", 200)\n\t\treturn newTestResponse(200, \"package custom1\"), nil\n\t}\n}\n\nfunc newTestResponse(status int, response string) *http.Response {\n\treturn &http.Response{\n\t\tStatusCode: status,\n\t\tBody:       ioutil.NopCloser(bytes.NewBufferString(response)),\n\t\t// Must be set to non-nil value or it panics\n\t\tHeader: make(http.Header),\n\t}\n}\n"
  },
  {
    "path": "router/integrations.go",
    "content": "package router\n\ntype ActionSettings struct {\n\tName                  string                       `json:\"name,omitempty\"`\n\tType                  string                       `json:\"type,omitempty\"`\n\tRunsOn                string                       `json:\"runs-on,omitempty\"`\n\tEnable                bool                         `json:\"enable,omitempty\"`\n\tUrl                   string                       `json:\"url,omitempty\"`\n\tUser                  string                       `json:\"user,omitempty\"`\n\tPassword              string                       `json:\"password,omitempty\"`\n\tTlsVerify             bool                         `json:\"tls-verify,omitempty\"`\n\tProjectKey            string                       `json:\"project-key,omitempty\" structs:\"project-key,omitempty\"`\n\tIssueType             string                       `json:\"issuetype,omitempty\" structs:\"issuetype\"`\n\tBoardName             string                       `json:\"board,omitempty\" structs:\"board,omitempty\"`\n\tPriority              string                       `json:\"priority,omitempty\"`\n\tAssignee              []string                     `json:\"assignee,omitempty\"`\n\tSummary               string                       `json:\"summary,omitempty\"`\n\tFixVersions           []string                     `json:\"fix-versions,omitempty\"`\n\tAffectsVersions       []string                     `json:\"affects-versions,omitempty\"`\n\tLabels                []string                     `json:\"labels,omitempty\"`\n\tSprint                string                       `json:\"sprint,omitempty\"`\n\tUnknowns              map[string]string            `json:\"unknowns,omitempty\" structs:\"unknowns,omitempty\"`\n\tHost                  string                       `json:\"host,omitempty\"`\n\tPort                  int                          `json:\"port,omitempty\"`\n\tRecipients            []string                     `json:\"recipients,omitempty\"`\n\tSender                string                       `json:\"sender,omitempty\"`\n\tToken                 string                       `json:\"token,omitempty\"`\n\tClientHostName        string                       `json:\"client-host-name,omitempty\"`\n\tUseMX                 bool                         `json:\"use-mx,omitempty\"`\n\tInstanceName          string                       `json:\"instance,omitempty\"`\n\tSizeLimit             int                          `json:\"size-limit,omitempty\"`\n\tInputFile             string                       `json:\"input-file,omitempty\"`\n\tExecScript            string                       `json:\"exec-script,omitempty\"`\n\tEnv                   []string                     `json:\"env,omitempty\"`\n\tBodyFile              string                       `json:\"body-file,omitempty\"`\n\tBodyContent           string                       `json:\"body-content,omitempty\"`\n\tMethod                string                       `json:\"method,omitempty\"`\n\tTimeout               string                       `json:\"timeout,omitempty\"`\n\tHeaders               map[string][]string          `json:\"headers,omitempty\"`\n\tOrganizationId        string                       `json:\"organization-id,omitempty\"`\n\tKubeConfigFile        string                       `json:\"kube-config-file,omitempty\"`\n\tKubeLabelSelector     string                       `json:\"kube-label-selector,omitempty\"`\n\tKubeActions           map[string]map[string]string `json:\"kube-actions,omitempty\"`\n\tKubeNamespace         string                       `json:\"kube-namespace,omitempty\"`\n\tDockerImageName       string                       `json:\"docker-image-name,omitempty\"`\n\tDockerNetwork         string                       `json:\"docker-network,omitempty\"`\n\tDockerCmd             []string                     `json:\"docker-cmd,omitempty\"`\n\tDockerVolumes         map[string]string            `json:\"docker-volume-mounts,omitempty\"`\n\tDockerEnv             []string                     `json:\"docker-env,omitempty\"`\n\tTags                  []string                     `json:\"tags,omitempty\"`\n\tAlias                 string                       `json:\"alias,omitempty\"`\n\tEntity                string                       `json:\"entity,omitempty\"`\n\tPagerdutyAuthToken    string                       `json:\"pagerduty-auth-token,omitempty\"`\n\tPagerdutyRoutingKey   string                       `json:\"pagerduty-routing-key,omitempty\"`\n\tDependencyTrackAPIKey string                       `json:\"dependency-track-api-key,omitempty\"`\n}\n"
  },
  {
    "path": "router/loads_test.go",
    "content": "package router\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/msgservice\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n)\n\ntype ctxWrapper struct {\n\tinstance           *Router\n\tsavedBaseForTicker time.Duration\n\tsavedGetService    func() service\n\tsavedDBPath        string\n\tcfgPath            string\n\tdefaultRegoFolder  string\n\tcommonRegoFolder   string\n\tbuff               chan invctn\n}\n\ntype invctn struct {\n\tactionCls   string\n\ttemplateCls string\n\trouteName   string\n}\n\nfunc (ctx *ctxWrapper) MsgHandling(input []byte, action actions.Action, route *routes.InputRoute, inpteval data.Inpteval, aquaServer *string) {\n\ti := invctn{\n\t\tfmt.Sprintf(\"%T\", action),\n\t\tfmt.Sprintf(\"%T\", inpteval),\n\t\troute.Name,\n\t}\n\tctx.buff <- i\n}\n\nfunc (ctxWrapper *ctxWrapper) setup(cfg string) {\n\tctxWrapper.savedDBPath = dbservice.DbPath\n\tctxWrapper.savedBaseForTicker = baseForTicker\n\tctxWrapper.cfgPath = \"cfg_test.yaml\"\n\tctxWrapper.savedGetService = getScanService\n\tctxWrapper.buff = make(chan invctn)\n\n\tdbservice.DbPath = \"test_webhooks.db\"\n\tbaseForTicker = time.Microsecond\n\tctxWrapper.defaultRegoFolder = \"rego-templates\"\n\tctxWrapper.commonRegoFolder = ctxWrapper.defaultRegoFolder + \"/common\"\n\terr := os.Mkdir(ctxWrapper.defaultRegoFolder, 0777)\n\tif err != nil {\n\t\tlog.Printf(\"Can't create %s %v\", ctxWrapper.defaultRegoFolder, err)\n\t}\n\terr = os.Mkdir(ctxWrapper.commonRegoFolder, 0777)\n\tif err != nil {\n\t\tlog.Printf(\"Can't create %s %v\", ctxWrapper.defaultRegoFolder, err)\n\t}\n\n\tgetScanService = func() service {\n\t\treturn ctxWrapper\n\t}\n\n\terr = ioutil.WriteFile(ctxWrapper.cfgPath, []byte(cfg), 0644)\n\tif err != nil {\n\t\tlog.Printf(\"Can't write to %s\", ctxWrapper.cfgPath)\n\t}\n\tctxWrapper.instance = Instance()\n}\n\nfunc (ctxWrapper *ctxWrapper) teardown() {\n\tctxWrapper.instance.Terminate()\n\n\tbaseForTicker = ctxWrapper.savedBaseForTicker\n\tos.Remove(ctxWrapper.cfgPath)\n\tos.Remove(dbservice.DbPath)\n\tos.Remove(ctxWrapper.commonRegoFolder)\n\tos.Remove(ctxWrapper.defaultRegoFolder)\n\n\tdbservice.ChangeDbPath(ctxWrapper.savedDBPath)\n\tgetScanService = ctxWrapper.savedGetService\n\tclose(ctxWrapper.buff)\n}\n\nfunc (ctx *ctxWrapper) EvaluateRegoRule(r *routes.InputRoute, _ []byte) bool {\n\tif r.Name == \"fail_evaluation\" {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc TestLoads(t *testing.T) {\n\tt.Skip(\"FIXME: this test makes an external call\")\n\tcfgData := `\nname: tenant\naqua-server: https://demolab.aquasec.com\nmax-db-size: 13MB #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\ndelete-old-data: 7 # delete data older than N day(s).  If empty then we do not delete.d\n\nroutes:\n- name: route1      #  name must be unique\n  input: |\n   contains(input.image, \"alpine\")\n   input.vulnerability_summary.critical >= 3\n\n  actions: [\"my-slack\"]        #  a list of integrations which will receive a scan or an audit event\n  template: raw       #  a template for this route\n  plugins:\n   policy-show-all: true\n\n- name: route2      #  name must be unique\n  input: |\n   contains(input.image, \"alpine\")\n\n  actions: [\"my-slack\"]        #  a list of integrations which will receive a scan or an audit event\n  template: raw       #  a template for this route\n  plugins:\n   policy-show-all: true\n\ntemplates:\n- name: raw\n  body: input\n\nactions:\n- name: splunk\n  type: splunk\n  enable: true\n  url: http://localhost:8088\n  token: 00aac750-a69c-4ebb-8771-41905f7369dd\n  size-limit: 1000\n\n- name: jira\n  type: jira\n  enable: true\n  url: \"https://afdesk.atlassian.net/\"\n  user: admin\n  password: admin\n  tls-verify: false\n  project-key: kcv`\n\n\twrap := ctxWrapper{}\n\twrap.setup(cfgData)\n\n\tdefer wrap.teardown()\n\n\tdemoCtx := wrap.instance\n\terr := demoCtx.Start(wrap.cfgPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\texpectedActionsCnt := 2\n\tif len(demoCtx.actions) != expectedActionsCnt {\n\t\tt.Errorf(\"There are stopped actions\\nWaited: %d\\nResult: %d\", expectedActionsCnt, len(demoCtx.actions))\n\t}\n\n\t_, ok := demoCtx.actions[\"jira\"]\n\tif !ok {\n\t\tt.Errorf(\"'jira' action didn't start!\")\n\t}\n\n\texpectedSrvUrl := \"https://demolab.aquasec.com/#/images/\"\n\tif demoCtx.aquaServer != expectedSrvUrl {\n\t\tt.Errorf(\"Wrong init of AquaServer link.\\nWait: %q\\nGot: %q\", expectedSrvUrl, demoCtx.aquaServer)\n\t}\n\n\tif _, ok := demoCtx.actions[\"splunk\"]; !ok {\n\t\tt.Errorf(\"Action 'splunk' didn't run!\")\n\t}\n}\nfunc TestReload(t *testing.T) {\n\tt.Skip(\"FIXME: this test makes an external call\")\n\tcfgData := `\nname: tenant\naqua-server: https://demolab.aquasec.com\nmax-db-size: 13MB #  Max size of DB. <numbers><unit suffix> pattern is used, such as \"300MB\" or \"1GB\". If empty or 0 then unlimited\ndelete-old-data: 7 # delete data older than N day(s).  If empty then we do not delete.d\n\nroutes:\n- name: route1      #  name must be unique\n  input: |\n   contains(input.image, \"alpine\")\n   input.vulnerability_summary.critical >= 3\n\n  actions: [\"my-slack\"]        #  a list of integrations which will receive a scan or an audit event\n  template: raw       #  a template for this route\n  plugins:\n   policy-show-all: true\n\n- name: route2      #  name must be unique\n  input: |\n   contains(input.image, \"alpine\")\n\n  actions: [\"my-slack\"]        #  a list of integrations which will receive a scan or an audit event\n  template: raw       #  a template for this route\n  plugins:\n   policy-show-all: true\n\ntemplates:\n- name: raw\n  body: input\n\nactions:\n- name: splunk\n  type: splunk\n  enable: true\n  url: http://localhost:8088\n  token: 00aac750-a69c-4ebb-8771-41905f7369dd\n  size-limit: 1000\n\n- name: jira\n  type: jira\n  enable: true\n  url: \"https://afdesk.atlassian.net/\"\n  user: admin\n  password: admin\n  tls-verify: false\n  project-key: kcv`\n\n\textraOtptCfg := `\n- name: jira2\n  type: jira\n  enable: true\n  url: \"https://afdesk.atlassian.net/\"\n  user: admin\n  password: admin\n  tls-verify: false\n  project-key: kcv`\n\n\twrap := ctxWrapper{}\n\twrap.setup(cfgData)\n\n\tdefer wrap.teardown()\n\n\tdemoCtx := wrap.instance\n\terrStart := demoCtx.Start(wrap.cfgPath)\n\tif errStart != nil {\n\t\tt.Fatal(errStart)\n\t}\n\texpectedActionsCnt := 2\n\tif len(demoCtx.actions) != expectedActionsCnt {\n\t\tt.Errorf(\"There are stopped actions\\nWaited: %d\\nResult: %d\", expectedActionsCnt, len(demoCtx.actions))\n\t}\n\n\tf, err := os.OpenFile(wrap.cfgPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n\tif err != nil {\n\t\tt.Errorf(\"Can't open config %v\\n\", err)\n\t}\n\tdefer f.Close()\n\tif _, err := f.WriteString(extraOtptCfg); err != nil {\n\t\tt.Errorf(\"Can't update config %v\\n\", err)\n\t}\n\tdemoCtx.ReloadConfig()\n\texpectedActionsAfterReload := 3\n\n\tif len(demoCtx.actions) != expectedActionsAfterReload {\n\t\tt.Errorf(\"There are stopped actions\\nWaited: %d\\nResult: %d\", expectedActionsAfterReload, len(demoCtx.actions))\n\t}\n\n}\n\nfunc TestServiceGetters(t *testing.T) {\n\tscanner := getScanService()\n\tif _, ok := scanner.(*msgservice.MsgService); !ok {\n\t\tt.Error(\"getScanService() doesn't return an instance of scanservice.ScanService\")\n\t}\n}\n"
  },
  {
    "path": "router/parsecfg.go",
    "content": "package router\n\nimport (\n\t\"bytes\"\n\t\"io/ioutil\"\n\t\"log\"\n\n\t\"github.com/ghodss/yaml\"\n)\n\nconst (\n\tv1Marker  = \"- type: common\"\n\tv1Warning = `\n\n\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n\n  Options supported only in Postee V1 are found in %s. Please make sure app is configured correctly!\n  See https://github.com/aquasecurity/postee/blob/main/README.md for the details.\n\n@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n\n\n`\n)\n\nfunc Parsev2cfg(cfgpath string) (*TenantSettings, error) {\n\tdata, err := ioutil.ReadFile(cfgpath)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to open file %s, %s\", cfgpath, err)\n\t\treturn nil, err\n\t}\n\n\tcheckV1Cfg(data, cfgpath)\n\n\ttenant := &TenantSettings{}\n\terr = yaml.Unmarshal(data, tenant)\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed yaml.Unmarshal, %s\", err)\n\t\treturn nil, err\n\t}\n\n\treturn tenant, nil\n\n}\nfunc checkV1Cfg(data []byte, cfgpath string) {\n\tif bytes.Index(data, []byte(v1Marker)) > -1 {\n\t\tlog.Printf(v1Warning, cfgpath)\n\t}\n}\n"
  },
  {
    "path": "router/parsecfg_test.go",
    "content": "package router\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestParseCfgWithInvalidFilename(t *testing.T) {\n\tinvalidfn := \"not-a-cfg.yaml\"\n\t_, err := Parsev2cfg(invalidfn)\n\n\tif err == nil {\n\t\tt.Errorf(\"Error is expected\")\n\t}\n}\n\nfunc TestParseCfgWithInvalidYaml(t *testing.T) {\n\tcfgfn := \"cfg.yaml\"\n\tinvalidYaml := `\nplaying_song_artist: Playing song, {{ song_name }} by {{ artist }}\n\nplaying_playlist: {{ action }} playlist {{ playlist_name }}`\n\tdefer func() {\n\t\tos.Remove(cfgfn)\n\t}()\n\n\terrWriteFile := ioutil.WriteFile(cfgfn, []byte(invalidYaml), 0644)\n\tif errWriteFile != nil {\n\t\tt.Errorf(\"Error in WriteFile: %s\", errWriteFile)\n\t}\n\n\t_, err := Parsev2cfg(cfgfn)\n\n\tif err == nil {\n\t\tt.Errorf(\"Error is expected\")\n\t}\n\n}\n"
  },
  {
    "path": "router/routehandling_test.go",
    "content": "package router\n\nimport (\n\t\"testing\"\n\t\"time\"\n)\n\nvar (\n\tsingleRoute string = `\nName: tenant\n\nroutes:\n- name: route1\n  actions: [\"my-slack\"]\n  template: raw\n  plugins:\n   Policy-Show-All: true\n\ntemplates:\n- name: raw\n  body: |\n   package postee\n   result:=input\n\nactions:\n- name: my-slack\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/TTT`\n\tnoAssociatedAction string = `\nName: tenant\n\nroutes:\n- name: route1\n  template: raw\n  plugins:\n   Policy-Show-All: true\n\ntemplates:\n- name: raw\n  body: |\n   package postee\n   result:=input\n\nactions:\n- name: my-slack\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/TTT`\n\ttwoRoutes string = `\nName: tenant\n\nroutes:\n- name: route1\n  actions: [\"my-slack\"]\n  template: raw\n  plugins:\n   Policy-Show-All: true\n\n- name: route2\n  actions: [\"my-slack\"]\n  template: raw\n  plugins:\n   Policy-Show-All: true\n\ntemplates:\n- name: raw\n  body: |\n   package postee\n   result:=input\n\nactions:\n- name: my-slack\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/TTT`\n\n\ttwoActions string = `\nName: tenant\n\nroutes:\n- name: route1\n  actions: [\"my-slack\", \"my-slack2\"]\n  template: raw\n  plugins:\n   Policy-Show-All: true\n\ntemplates:\n- name: raw\n  body: |\n   package postee\n   result:=input\n\nactions:\n- name: my-slack\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/XXX\n- name: my-slack2\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/TTT`\n\tnoActions string = `\nName: tenant\n\nroutes:\n- name: route1\n  actions: [\"my-slack3\"]\n  template: raw\n  plugins:\n   Policy-Show-All: true\n\ntemplates:\n- name: raw\n  body: |\n   package postee\n   result:=input`\n\tnoTemplates string = `\nName: tenant\n\nroutes:\n- name: route1\n  actions: [\"my-slack\", \"my-slack2\"]\n  template: raw\n  plugins:\n   Policy-Show-All: true\n\nactions:\n- name: my-slack\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/XXX\n- name: my-slack2\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/TTT`\n\tinvalidTemplate string = `\nName: tenant\n\nroutes:\n- name: route1\n  actions: [\"my-slack\"]\n  template: rawx\n  plugins:\n   Policy-Show-All: true\n\ntemplates:\n- name: raw\n  body: |\n   package postee\n   result:=input\n\nactions:\n- name: my-slack\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/TTT`\n\tinvalidAction string = `\nName: tenant\n\nroutes:\n- name: route1\n  actions: [\"x-slack\"]\n  template: raw\n  plugins:\n   Policy-Show-All: true\n\ntemplates:\n- name: raw\n  body: |\n   package postee\n   result:=input\n\nactions:\n- name: my-slack\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/TTT`\n\tsingleRouteSingelInput string = `\nName: tenant\n\nroutes:\n- name: fail_evaluation\n  actions: [\"my-slack\"]\n  template: raw\n  input-files:\n   - Allow-Registry.rego\n\ntemplates:\n- name: raw\n  body: |\n   package postee\n   result:=input\n\nactions:\n- name: my-slack\n  type: slack\n  enable: true\n  url: https://hooks.slack.com/services/ABCDF/1234/TTT`\n\tpayload = `{\"image\" : \"alpine\"}`\n)\n\nfunc TestHandling(t *testing.T) {\n\ttests := []struct {\n\t\tcaseDesc      string\n\t\tcfg           string\n\t\texpctdInvctns []invctn\n\t}{\n\t\t{\n\t\t\t\"Single Route\",\n\t\t\tsingleRoute,\n\t\t\t[]invctn{\n\t\t\t\t{\n\t\t\t\t\t\"*actions.SlackAction\", \"*regoservice.regoEvaluator\", \"route1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"2 Routes\",\n\t\t\ttwoRoutes,\n\t\t\t[]invctn{\n\t\t\t\t{\n\t\t\t\t\t\"*actions.SlackAction\", \"*regoservice.regoEvaluator\", \"route1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"*actions.SlackAction\", \"*regoservice.regoEvaluator\", \"route2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"2 Actions per single route\",\n\t\t\ttwoActions,\n\t\t\t[]invctn{\n\t\t\t\t{\n\t\t\t\t\t\"*actions.SlackAction\", \"*regoservice.regoEvaluator\", \"route1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\t\"*actions.SlackAction\", \"*regoservice.regoEvaluator\", \"route1\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"No Actions configured\",\n\t\t\tnoActions,\n\t\t\t[]invctn{},\n\t\t},\n\t\t{\n\t\t\t\"No Template configured\",\n\t\t\tnoTemplates,\n\t\t\t[]invctn{},\n\t\t},\n\t\t{\n\t\t\t\"Invalid Action reference\",\n\t\t\tinvalidAction,\n\t\t\t[]invctn{},\n\t\t},\n\t\t{\n\t\t\t\"Invalid Template reference\",\n\t\t\tinvalidTemplate,\n\t\t\t[]invctn{},\n\t\t},\n\t\t{\n\t\t\t\"No actions associated with route\",\n\t\t\tnoAssociatedAction,\n\t\t\t[]invctn{},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\trunTestRouteHandlingCase(t, test.caseDesc, test.cfg, test.expctdInvctns)\n\t}\n}\nfunc runTestRouteHandlingCase(t *testing.T, caseDesc string, cfg string, expctdInvctns []invctn) {\n\tactualInvctCnt := 0\n\tt.Logf(\"Case: %s\\n\", caseDesc)\n\twrap := ctxWrapper{}\n\twrap.setup(cfg)\n\n\tdefer wrap.teardown()\n\n\terr := wrap.instance.Start(wrap.cfgPath)\n\tif err != nil {\n\t\tt.Fatalf(\"[%s] Unexpected error %v\", caseDesc, err)\n\t}\n\n\twrap.instance.handle([]byte(payload))\n\ttimeoutDuration := 3 * time.Second\n\tif len(expctdInvctns) == 0 {\n\t\ttimeoutDuration = time.Second\n\t}\n\ttimeout := time.After(timeoutDuration)\n\tfor {\n\t\tselect {\n\t\tcase <-timeout:\n\t\t\tif len(expctdInvctns) > 0 {\n\t\t\t\tt.Fatal(\"test didn't finish in time\")\n\t\t\t}\n\t\t\treturn\n\t\tcase r := <-wrap.buff:\n\t\t\tt.Logf(\"[%s] received invocation (%s, %s, %s)\", caseDesc, r.routeName, r.actionCls, r.templateCls)\n\t\t\tactualInvctCnt++\n\t\t\tfound := false\n\t\t\tfor _, expect := range expctdInvctns {\n\t\t\t\tif r == expect {\n\t\t\t\t\tfound = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tif actualInvctCnt == len(expctdInvctns) {\n\t\t\t\treturn //everything is ok, exiting\n\t\t\t}\n\t\t\tif !found && len(expctdInvctns) > 0 {\n\t\t\t\tt.Errorf(\"[%s] Unexpected invocation (%s, %s, %s)\", caseDesc, r.routeName, r.actionCls, r.templateCls)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif actualInvctCnt > len(expctdInvctns) {\n\t\t\t\tt.Errorf(\"[%s] Service should be called %d times but called %d times\", caseDesc, len(expctdInvctns), actualInvctCnt)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n}\nfunc TestInvalidRouteName(t *testing.T) {\n\texpctdInvctns := 0\n\tactualInvctCnt := 0\n\twrap := ctxWrapper{}\n\twrap.setup(singleRoute)\n\n\tdefer wrap.teardown()\n\n\terr := wrap.instance.Start(wrap.cfgPath)\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error %v\", err)\n\t}\n\n\twrap.instance.HandleRoute(\"not-exist\", []byte(payload))\n\ttimeout := time.After(1 * time.Second)\n\tfor {\n\t\tselect {\n\t\tcase <-timeout:\n\t\t\treturn\n\t\tcase <-wrap.buff:\n\t\t\tactualInvctCnt++\n\t\t\tif actualInvctCnt > expctdInvctns {\n\t\t\t\tt.Errorf(\"Service shouldn't be called if invalid route is specified\")\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestRouteWithNoValidRego(t *testing.T) {\n\texpctdInvctns := 0\n\tactualInvctCnt := 0\n\twrap := ctxWrapper{}\n\twrap.setup(singleRouteSingelInput)\n\n\tdefer wrap.teardown()\n\n\terr := wrap.instance.Start(wrap.cfgPath)\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error %v\", err)\n\t}\n\n\twrap.instance.HandleRoute(\"fail_evaluation\", []byte(payload))\n\ttimeout := time.After(1 * time.Second)\n\tfor {\n\t\tselect {\n\t\tcase <-timeout:\n\t\t\treturn\n\t\tcase <-wrap.buff:\n\t\t\tactualInvctCnt++\n\t\t\tif actualInvctCnt > expctdInvctns {\n\t\t\t\tt.Errorf(\"Service shouldn't be called if invalid route is specified\")\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestSend(t *testing.T) {\n\texpctdInvctns := 1\n\tactualInvctCnt := 0\n\twrap := ctxWrapper{}\n\twrap.setup(singleRoute)\n\n\tdefer wrap.teardown()\n\n\terr := wrap.instance.Start(wrap.cfgPath)\n\tif err != nil {\n\t\tt.Fatalf(\"Unexpected error %v\", err)\n\t}\n\n\twrap.instance.Send([]byte(payload))\n\ttimeout := time.After(1 * time.Second)\n\tfor {\n\t\tselect {\n\t\tcase <-timeout:\n\t\t\treturn\n\t\tcase <-wrap.buff:\n\t\t\tactualInvctCnt++\n\t\t\tif actualInvctCnt != expctdInvctns {\n\t\t\t\tt.Errorf(\"Service shouldn't be called once\")\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "router/router.go",
    "content": "package router\n\nimport (\n\t\"bytes\"\n\t\"container/ring\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"path\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee/v2/msgservice\"\n\t\"github.com/aquasecurity/postee/v2/regoservice\"\n\t\"github.com/aquasecurity/postee/v2/routes\"\n\t\"github.com/aquasecurity/postee/v2/utils\"\n\t\"github.com/ghodss/yaml\"\n\t\"github.com/nats-io/nats-server/v2/server\"\n\t\"github.com/nats-io/nats.go\"\n)\n\nconst (\n\tServiceNowTableDefault = \"incident\"\n\tAnonymizeReplacement   = \"<hidden>\"\n)\n\ntype Router struct {\n\tConfigCh      chan *nats.Msg\n\tMode          string\n\tNatsServer    *server.Server\n\tNatsConn      *nats.Conn\n\tNatsMsgCh     chan *nats.Msg\n\tRunnerName    string\n\tControllerURL string\n\n\tmutexScan       sync.Mutex\n\tquit            chan struct{}\n\treadOnlyEvents  *ring.Ring\n\tinputEventQueue chan []byte\n\tticker          *time.Ticker\n\tstopTicker      chan struct{}\n\tcfgfile         string\n\taquaServer      string\n\tactions         map[string]actions.Action\n\tinputRoutes     map[string]*routes.InputRoute\n\ttemplates       map[string]data.Inpteval\n}\n\nvar (\n\tinitCtx       sync.Once\n\trouterCtx     *Router\n\tbaseForTicker = time.Hour\n\n\trequireAuthorization = map[string]bool{\n\t\t\"servicenow\": true,\n\t}\n)\n\nfunc Instance() *Router {\n\tinitCtx.Do(func() {\n\t\trouterCtx = &Router{\n\t\t\tmutexScan:       sync.Mutex{},\n\t\t\tquit:            make(chan struct{}),\n\t\t\treadOnlyEvents:  ring.New(1000),\n\t\t\tinputEventQueue: make(chan []byte, 1000),\n\t\t\tactions:         make(map[string]actions.Action),\n\t\t\tinputRoutes:     make(map[string]*routes.InputRoute),\n\t\t\ttemplates:       make(map[string]data.Inpteval),\n\t\t\tstopTicker:      make(chan struct{}),\n\t\t}\n\t})\n\treturn routerCtx\n}\nfunc (ctx *Router) ReloadConfig() {\n\tctx.Terminate()\n\terr := ctx.Start(ctx.cfgfile)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to start router: %s\", err)\n\t}\n}\n\nfunc (ctx *Router) Start(cfgfile string) error {\n\tlog.Printf(\"Starting Router....\")\n\n\tctx.cfgfile = cfgfile\n\tctx.actions = map[string]actions.Action{}\n\tctx.inputRoutes = map[string]*routes.InputRoute{}\n\tctx.templates = map[string]data.Inpteval{}\n\tctx.ticker = nil\n\n\terr := ctx.load()\n\tif err != nil {\n\t\treturn err\n\t}\n\tgo ctx.listen()\n\treturn nil\n}\n\nfunc (ctx *Router) Terminate() {\n\tlog.Printf(\"Terminating Router....\")\n\n\tif ctx.NatsConn != nil {\n\t\tlog.Println(\"Closing NATS connection\")\n\t\tctx.NatsConn.Close()\n\t\tlog.Println(\"NATS termination complete\")\n\t}\n\n\tfor _, pl := range ctx.actions {\n\t\terr := pl.Terminate()\n\t\tif err != nil {\n\t\t\tlog.Printf(\"failed to terminate action: %v\", err)\n\t\t}\n\t}\n\tlog.Printf(\"Actions terminated\")\n\n\tfor _, route := range ctx.inputRoutes {\n\t\troute.StopScheduler()\n\t}\n\tlog.Printf(\"Route schedulers stopped\")\n\n\tctx.quit <- struct{}{}\n\tlog.Printf(\"quit notified\")\n\tif ctx.ticker != nil {\n\t\tctx.stopTicker <- struct{}{}\n\t\tlog.Printf(\"stopTicker notified\")\n\t}\n\n}\n\nfunc (ctx *Router) Send(data []byte) {\n\tctx.inputEventQueue <- data\n\tctx.readOnlyEvents.Value = data\n\tctx.readOnlyEvents = ctx.readOnlyEvents.Next()\n}\n\nfunc (ctx *Router) GetCurrentEvents() []any {\n\tvar events []any\n\tctx.readOnlyEvents.Do(func(a any) {\n\t\tevents = append(events, a)\n\t})\n\treturn events\n}\n\nfunc (ctx *Router) initTemplate(template *Template) error {\n\tlog.Printf(\"Configuring template %s \\n\", template.Name)\n\n\tif template.LegacyScanRenderer != \"\" {\n\t\tinpteval, err := formatting.BuildLegacyScnEvaluator(template.LegacyScanRenderer)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.templates[template.Name] = inpteval\n\t\tlog.Printf(\"Configured with legacy renderer %s \\n\", template.LegacyScanRenderer)\n\t}\n\n\tif template.RegoPackage != \"\" {\n\t\tinpteval, err := regoservice.BuildBundledRegoEvaluator(template.RegoPackage)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.templates[template.Name] = inpteval\n\t\tlog.Printf(\"Configured with Rego package %s\\n\", template.RegoPackage)\n\t}\n\tif template.Url != \"\" {\n\t\tlog.Printf(\"Configured with url: %s\\n\", template.Url)\n\n\t\tr, err := http.NewRequest(\"GET\", template.Url, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\thttpClient := getHttpClient()\n\t\tresp, err := httpClient.Do(r)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif resp.StatusCode > 399 {\n\t\t\treturn errors.New(fmt.Sprintf(\"can not connect to %s, response status is %d\", template.Url, resp.StatusCode))\n\t\t}\n\n\t\tb, err := ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\tinpteval, err := regoservice.BuildExternalRegoEvaluator(path.Base(r.URL.Path), string(b))\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tctx.templates[template.Name] = inpteval\n\t}\n\t//body goes last to provide an option to keep body in config but not use it\n\tif template.Body != \"\" {\n\t\tinpteval, err := regoservice.BuildExternalRegoEvaluator(\"inline.rego\", template.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tctx.templates[template.Name] = inpteval\n\t}\n\treturn nil\n}\n\nfunc (ctx *Router) load() error {\n\tctx.mutexScan.Lock()\n\tdefer ctx.mutexScan.Unlock()\n\tlog.Printf(\"Loading alerts configuration file %s ....\\n\", ctx.cfgfile)\n\ttenant, err := Parsev2cfg(ctx.cfgfile)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(tenant.AquaServer) > 0 {\n\t\tvar slash string\n\t\tif !strings.HasSuffix(tenant.AquaServer, \"/\") {\n\t\t\tslash = \"/\"\n\t\t}\n\t\tctx.aquaServer = fmt.Sprintf(\"%s%s#/images/\", tenant.AquaServer, slash)\n\t}\n\n\tdbservice.DbSizeLimit = parseSize(tenant.DBMaxSize)\n\tif tenant.DBTestInterval == 0 {\n\t\ttenant.DBTestInterval = 1\n\t}\n\tctx.ticker = time.NewTicker(baseForTicker * time.Duration(tenant.DBTestInterval))\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.stopTicker:\n\t\t\t\treturn\n\t\t\tcase <-ctx.ticker.C:\n\t\t\t\tdbservice.CheckSizeLimit()\n\t\t\t\tdbservice.CheckExpiredData()\n\t\t\t}\n\t\t}\n\t}()\n\n\tfor i, r := range tenant.InputRoutes {\n\t\tctx.inputRoutes[r.Name] = routes.ConfigureTimeouts(&tenant.InputRoutes[i])\n\t}\n\tfor _, t := range tenant.Templates {\n\t\terr := ctx.initTemplate(&t)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Can not initialize template %s: %v \\n\", t.Name, err)\n\t\t}\n\t}\n\n\tfor _, settings := range tenant.Actions {\n\t\tutils.Debug(\"%#v\\n\", anonymizeSettings(&settings))\n\n\t\tif settings.Enable {\n\t\t\tplg := BuildAndInitOtpt(&settings, ctx.aquaServer)\n\t\t\tif plg != nil {\n\t\t\t\tlog.Printf(\"Action %s is configured\", settings.Name)\n\t\t\t\tctx.actions[settings.Name] = plg\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype service interface {\n\tMsgHandling(input []byte, output actions.Action, route *routes.InputRoute, inpteval data.Inpteval, aquaServer *string)\n\tEvaluateRegoRule(input *routes.InputRoute, in []byte) bool\n}\n\nvar getScanService = func() service {\n\tserv := &msgservice.MsgService{}\n\treturn serv\n}\nvar getHttpClient = func() *http.Client {\n\treturn http.DefaultClient\n}\n\nfunc (ctx *Router) HandleRoute(routeName string, in []byte) {\n\tr, ok := ctx.inputRoutes[routeName]\n\tif !ok || r == nil {\n\t\tlog.Printf(\"No route found: %q\", routeName)\n\t\treturn\n\t}\n\tif len(r.Actions) == 0 {\n\t\tlog.Printf(\"Route %q has no actions\", routeName)\n\t\treturn\n\t}\n\n\t// send event up to controller unconditionally, in case controller knows\n\tif ctx.Mode == \"runner\" {\n\t\tlog.Println(\"Sending event upstream to controller at url: \", ctx.ControllerURL)\n\t\tNATSEventSubject := \"postee.events\"\n\t\tif err := ctx.NatsConn.Publish(NATSEventSubject, in); err != nil { // TODO: What happens if controller is unavailable?\n\t\t\tlog.Println(\"Unable to send event upstream to controller at url: \", ctx.ControllerURL, \"err: \", err.Error())\n\t\t}\n\t}\n\n\tif !getScanService().EvaluateRegoRule(r, in) {\n\t\treturn\n\t}\n\n\tfor _, ra := range r.Actions {\n\t\thandle := true\n\t\tif ctx.Mode == \"controller\" {\n\t\t\tcontroller, err := Parsev2cfg(ctx.cfgfile)\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"Unable to parse cfgfile for controller: \", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor _, ca := range controller.Actions {\n\t\t\t\tif ra == ca.Name {\n\t\t\t\t\tif ca.RunsOn != \"\" {\n\t\t\t\t\t\tlog.Println(\"Skipping: \", ca.Name, \"as it is for runner: \", ca.RunsOn)\n\t\t\t\t\t\thandle = false\n\t\t\t\t\t\tbreak // skip as it is for runner to run\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif !handle {\n\t\t\tcontinue\n\t\t}\n\n\t\tpl, ok := ctx.actions[ra]\n\t\tif !ok {\n\t\t\tlog.Printf(\"route %q contains an action %q, which isn't enabled now.\", routeName, ra)\n\t\t\tcontinue\n\t\t}\n\t\ttmpl, ok := ctx.templates[r.Template]\n\t\tif !ok {\n\t\t\tlog.Printf(\"route %q contains reference to undefined or misconfigured template %q.\",\n\t\t\t\trouteName, r.Template)\n\t\t\tcontinue\n\t\t}\n\t\tlog.Printf(\"route %q is associated with template %q\", routeName, r.Template)\n\n\t\tif r.SerializeActions {\n\t\t\tgetScanService().MsgHandling(in, pl, r, tmpl, &ctx.aquaServer)\n\t\t} else {\n\t\t\tgo getScanService().MsgHandling(in, pl, r, tmpl, &ctx.aquaServer)\n\t\t}\n\t}\n}\n\nfunc (ctx *Router) handle(in []byte) {\n\tfor routeName := range ctx.inputRoutes {\n\t\tctx.HandleRoute(routeName, in)\n\t}\n}\nfunc BuildAndInitOtpt(settings *ActionSettings, aquaServerUrl string) actions.Action {\n\tsettings.User = utils.GetEnvironmentVarOrPlain(settings.User)\n\tif len(settings.User) == 0 && requireAuthorization[settings.Type] {\n\t\tlog.Printf(\"User for %q is empty\", settings.Name)\n\t\treturn nil\n\t}\n\tsettings.Password = utils.GetEnvironmentVarOrPlain(settings.Password)\n\tif len(settings.Password) == 0 && requireAuthorization[settings.Type] {\n\t\tlog.Printf(\"Password for %q is empty\", settings.Name)\n\t\treturn nil\n\t}\n\tsettings.Token = utils.GetEnvironmentVarOrPlain(settings.Token)\n\tif settings.Type == \"jira\" {\n\t\tif len(settings.User) == 0 {\n\t\t\tlog.Printf(\"User for %q is empty\", settings.Name)\n\t\t\treturn nil\n\t\t}\n\t\tif len(settings.Token) == 0 && len(settings.Password) == 0 {\n\t\t\tlog.Printf(\"Password and Token for %q are empty\", settings.Name)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tutils.Debug(\"Starting Action %q: %q\\n\", settings.Type, settings.Name)\n\n\tvar plg actions.Action\n\tvar err error\n\n\tswitch strings.ToLower(settings.Type) {\n\tcase \"jira\":\n\t\tplg = buildJiraAction(settings)\n\tcase \"email\":\n\t\tplg = buildEmailAction(settings)\n\tcase \"slack\":\n\t\tplg = buildSlackAction(settings, aquaServerUrl)\n\tcase \"teams\":\n\t\tplg = buildTeamsAction(settings, aquaServerUrl)\n\tcase \"servicenow\":\n\t\tplg = buildServiceNow(settings)\n\tcase \"webhook\":\n\t\tplg = buildWebhookAction(settings)\n\tcase \"splunk\":\n\t\tplg = buildSplunkAction(settings)\n\tcase \"stdout\":\n\t\tplg = buildStdoutAction(settings)\n\tcase \"nexusiq\":\n\t\tplg = buildNexusIqAction(settings)\n\tcase \"dependencytrack\":\n\t\tplg = buildDependencyTrackAction(settings)\n\tcase \"opsgenie\":\n\t\tplg = buildOpsGenieAction(settings)\n\tcase \"exec\":\n\t\tplg, err = buildExecAction(settings)\n\t\tif err != nil {\n\t\t\tlog.Println(err.Error())\n\t\t\treturn nil\n\t\t}\n\tcase \"http\":\n\t\tplg, err = buildHTTPAction(settings)\n\t\tif err != nil {\n\t\t\tlog.Println(err.Error())\n\t\t\treturn nil\n\t\t}\n\tcase \"kubernetes\":\n\t\tplg, err = buildKubernetesAction(settings)\n\t\tif err != nil {\n\t\t\tlog.Println(err.Error())\n\t\t\treturn nil\n\t\t}\n\tcase \"docker\":\n\t\tplg, err = buildDockerAction(settings)\n\t\tif err != nil {\n\t\t\tlog.Println(err.Error())\n\t\t\treturn nil\n\t\t}\n\tcase \"awssecurityhub\":\n\t\tplg, err = buildAWSSecurityHubAction(settings)\n\t\tif err != nil {\n\t\t\tlog.Println(err.Error())\n\t\t\treturn nil\n\t\t}\n\tcase \"pagerduty\":\n\t\tplg, err = buildPagerdutyAction(settings)\n\t\tif err != nil {\n\t\t\tlog.Println(err.Error())\n\t\t\treturn nil\n\t\t}\n\tdefault:\n\t\tlog.Printf(\"Action type %q is undefined or empty. Action name is %q.\",\n\t\t\tsettings.Type, settings.Name)\n\t\treturn nil\n\t}\n\n\terr = plg.Init()\n\tif err != nil {\n\t\tlog.Printf(\"failed to Init : %v\", err)\n\t\treturn nil\n\t}\n\n\treturn plg\n}\n\nfunc (ctx *Router) listen() {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.quit:\n\t\t\treturn\n\t\tcase data := <-ctx.inputEventQueue:\n\t\t\tgo ctx.handle(bytes.ReplaceAll(data, []byte{'`'}, []byte{'\\''}))\n\t\tcase msg := <-ctx.ConfigCh:\n\t\t\tlog.Println(\"A runner requested config: \", string(msg.Data))\n\t\t\tcfg, err := buildRunnerConfig(string(msg.Data), ctx.cfgfile)\n\t\t\tif err != nil {\n\t\t\t\tlog.Println(\"Failed to build config to send to runner: \", string(msg.Data), \"err: \", err)\n\t\t\t}\n\t\t\tif err = msg.Respond([]byte(cfg)); err != nil {\n\t\t\t\tlog.Println(\"Failed to send config to runner: \", err)\n\t\t\t}\n\t\tcase msg := <-ctx.NatsMsgCh:\n\t\t\t// TODO: Add logging to capture all received events\n\t\t\tlog.Println(\"Received incoming event from runner: \", string(msg.Data))\n\t\t\tgo ctx.handle(bytes.ReplaceAll(msg.Data, []byte{'`'}, []byte{'\\''}))\n\t\t}\n\t}\n}\n\n// TODO: Improve parsing logic\nfunc buildRunnerConfig(runnerName, cfgFile string) (string, error) {\n\ttenant, err := Parsev2cfg(cfgFile)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar runnerRoutes []routes.InputRoute\n\tvar runnerActions []ActionSettings\n\tvar runnerTemplates []Template\n\n\tfor _, output := range tenant.Actions {\n\t\tif output.RunsOn == runnerName {\n\t\t\trunnerActions = append(runnerActions, output)\n\t\t}\n\t}\n\n\tfor _, ro := range runnerActions {\n\t\tfor _, inputRoute := range tenant.InputRoutes {\n\t\t\tfor _, inputAction := range inputRoute.Actions {\n\t\t\t\tif ro.Name == inputAction {\n\t\t\t\t\trunnerRoute := inputRoute\n\t\t\t\t\tvar oNames []string\n\t\t\t\t\tfor _, o := range runnerActions {\n\t\t\t\t\t\toNames = append(oNames, o.Name)\n\t\t\t\t\t}\n\t\t\t\t\trunnerRoute.Actions = oNames\n\t\t\t\t\trunnerRoutes = append(runnerRoutes, runnerRoute)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, rr := range runnerRoutes {\n\t\tfor _, inputTemplate := range tenant.Templates {\n\t\t\tif inputTemplate.Name == rr.Template && !contains(runnerTemplates, inputTemplate.Name) {\n\t\t\t\trunnerTemplates = append(runnerTemplates, inputTemplate)\n\t\t\t}\n\t\t}\n\t}\n\n\ttenant.InputRoutes = runnerRoutes\n\ttenant.Actions = runnerActions\n\ttenant.Templates = runnerTemplates\n\n\tcfgB, err := yaml.Marshal(tenant)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(cfgB), nil\n}\n\nfunc contains(haystack []Template, needle string) bool {\n\tfor _, noodle := range haystack {\n\t\tif noodle.Name == needle {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc SetupConnOptions(opts []nats.Option) []nats.Option {\n\ttotalWait := 10 * time.Minute\n\treconnectDelay := 2 * time.Second\n\n\topts = append(opts, nats.ReconnectWait(reconnectDelay))\n\topts = append(opts, nats.MaxReconnects(int(totalWait/reconnectDelay)))\n\topts = append(opts, nats.DisconnectErrHandler(func(nc *nats.Conn, err error) {\n\t\tlog.Printf(\"Disconnected due to: %s, will attempt reconnects for %.0fm\", err, totalWait.Minutes())\n\t}))\n\topts = append(opts, nats.ReconnectHandler(func(nc *nats.Conn) {\n\t\tlog.Printf(\"Reconnected [%s]\", nc.ConnectedUrl())\n\t}))\n\treturn opts\n}\n"
  },
  {
    "path": "router/router_test.go",
    "content": "package router\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_buildRunnerConfig(t *testing.T) {\n\ttestCases := []struct {\n\t\tname          string\n\t\tcfgFile       string\n\t\twant          string\n\t\texpectedError string\n\t}{\n\t\t{\n\t\t\tname:    \"happy path\",\n\t\t\tcfgFile: \"goldens/sample.cfg\",\n\t\t\twant: `actions:\n- enable: true\n  env:\n  - MY_ENV_VAR=foo_bar_baz\n  - MY_KEY=secret\n  exec-script: |\n    #!/bin/sh\n    echo $POSTEE_EVENT\n    echo \"this is hello from postee\"\n  name: my-exec-from-runner\n  runs-on: postee-runner-1\n  type: exec\n- body-content: |\n    This is an another example of a inline body\n    Event ID: event.input.SigMetadata.ID\n  enable: true\n  method: POST\n  name: my-http-post-from-runner\n  runs-on: postee-runner-1\n  type: http\n  url: https://webhook.site/<uuid>\ndb-verify-interval: 1\nmax-db-size: 1000MB\nroutes:\n- actions:\n  - my-exec-from-runner\n  - my-http-post-from-runner\n  input: contains(input.SigMetadata.ID, \"TRC-1\")\n  name: runner-only-route\n  plugins: {}\n  serialize-actions: true\n  template: raw-json\n- actions:\n  - my-exec-from-runner\n  - my-http-post-from-runner\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  name: controller-runner-route\n  plugins: {}\n  serialize-actions: true\n  template: raw-json\n- actions:\n  - my-exec-from-runner\n  - my-http-post-from-runner\n  input: contains(input.SigMetadata.ID, \"TRC-1\")\n  name: runner-only-route\n  plugins: {}\n  serialize-actions: true\n  template: raw-json\n- actions:\n  - my-exec-from-runner\n  - my-http-post-from-runner\n  input: contains(input.SigMetadata.ID, \"TRC-2\")\n  name: controller-runner-route\n  plugins: {}\n  serialize-actions: true\n  template: raw-json\ntemplates:\n- name: raw-json\n  rego-package: postee.rawmessage.json`,\n\t\t},\n\t\t{\n\t\t\tname:          \"sad path, config not found\",\n\t\t\tcfgFile:       \"invalid path\",\n\t\t\texpectedError: \"open invalid path: no such file or directory\",\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tgot, err := buildRunnerConfig(\"postee-runner-1\", tc.cfgFile)\n\t\t\tswitch {\n\t\t\tcase tc.expectedError != \"\":\n\t\t\t\tassert.Equal(t, tc.expectedError, err.Error(), tc.name)\n\t\t\t\tassert.Empty(t, got, tc.name)\n\t\t\tdefault:\n\t\t\t\tassert.NoError(t, err, tc.name)\n\t\t\t\tassert.YAMLEq(t, tc.want, got, tc.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "router/rule.go",
    "content": "package router\n\ntype Rule struct {\n\tName string `json:\"name,omitempty\"`\n\tID   string `json:\"id,omitempty\"`\n}\n"
  },
  {
    "path": "router/sizeparser.go",
    "content": "package router\n\nimport (\n\t\"log\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tB  = 1\n\tKB = 1024\n\tMB = 1024 * KB\n\tGB = 1024 * MB\n)\n\nvar (\n\tsizeRegex = regexp.MustCompile(`^(\\d+) ?([kKmMgG]?[bB]?)$`)\n\tsuffixes  = map[string]int{\"b\": B, \"kb\": KB, \"mb\": MB, \"gb\": GB}\n\n\tparseError = \"unable parse MaxDBSize, unlimited size used\"\n)\n\nfunc parseSize(sizeStr string) int {\n\tif sizeStr == \"\" {\n\t\treturn 0\n\t}\n\n\tmatches := sizeRegex.FindStringSubmatch(sizeStr)\n\n\tif matches != nil {\n\t\tsize, err := strconv.Atoi(matches[1])\n\t\tif err != nil {\n\t\t\tlog.Println(parseError)\n\t\t\treturn 0\n\t\t}\n\t\tif matches[2] != \"\" {\n\t\t\tsuffix := suffixes[strings.ToLower(matches[2])]\n\t\t\treturn size * suffix\n\t\t} else {\n\t\t\treturn size\n\t\t}\n\t} else {\n\t\tlog.Println(parseError)\n\t\treturn 0\n\t}\n}\n"
  },
  {
    "path": "router/sizeparser_test.go",
    "content": "package router\n\nimport (\n\t\"fmt\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"math\"\n\t\"testing\"\n)\n\nfunc TestParseSize(t *testing.T) {\n\ttests := []struct {\n\t\tname       string\n\t\tsizeString string\n\t\twantSize   int\n\t}{\n\t\t{\n\t\t\tname:       \"happy path(empty string is used)\",\n\t\t\tsizeString: \"\",\n\t\t\twantSize:   0,\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path(suffix 'b' is used)\",\n\t\t\tsizeString: \"1b\",\n\t\t\twantSize:   1,\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path(suffix 'kb' is used)\",\n\t\t\tsizeString: \"2kb\",\n\t\t\twantSize:   2 * KB,\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path(suffix 'Mb' is used)\",\n\t\t\tsizeString: \"3Mb\",\n\t\t\twantSize:   3 * MB,\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path(suffix 'GB' is used)\",\n\t\t\tsizeString: \"4GB\",\n\t\t\twantSize:   4 * GB,\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path(suffix ' b' is used)\",\n\t\t\tsizeString: \"5 b\",\n\t\t\twantSize:   5,\n\t\t},\n\t\t{\n\t\t\tname:       \"happy path(suffix is not used)\",\n\t\t\tsizeString: \"6\",\n\t\t\twantSize:   6,\n\t\t},\n\t\t{\n\t\t\tname:       \"sad path(suffix 'tb' is used)\",\n\t\t\tsizeString: \"7TB\",\n\t\t\twantSize:   0,\n\t\t},\n\t\t{\n\t\t\tname:       \"sad path(float value is used)\",\n\t\t\tsizeString: \"8.8\",\n\t\t\twantSize:   0,\n\t\t},\n\t\t{\n\t\t\tname:       \"sad path(value more than MaxInt)\",\n\t\t\tsizeString: fmt.Sprintf(\"%d1\", math.MaxInt),\n\t\t\twantSize:   0,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsize := parseSize(test.sizeString)\n\n\t\t\tassert.EqualValues(t, test.wantSize, size)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "router/template.go",
    "content": "package router\n\ntype Template struct {\n\tName               string `json:\"name,omitempty\"`\n\tBody               string `json:\"body,omitempty\"`\n\tRegoPackage        string `json:\"rego-package,omitempty\"`\n\tLegacyScanRenderer string `json:\"legacy-scan-renderer,omitempty\"`\n\tUrl                string `json:\"url,omitempty\"`\n}\n"
  },
  {
    "path": "router/tenants.go",
    "content": "package router\n\nimport (\n\t\"github.com/aquasecurity/postee/v2/routes\"\n)\n\ntype TenantSettings struct {\n\tAquaServer      string              `json:\"aqua-server,omitempty\"`\n\tDBMaxSize       string              `json:\"max-db-size,omitempty\"`\n\tDBRemoveOldData int                 `json:\"delete-old-data,omitempty\"`\n\tDBTestInterval  int                 `json:\"db-verify-interval,omitempty\"`\n\tActions         []ActionSettings    `json:\"actions,omitempty\"`\n\tInputRoutes     []routes.InputRoute `json:\"routes,omitempty\"`\n\tTemplates       []Template          `json:\"templates,omitempty\"`\n\tRules           []Rule              `json:\"rules,omitempty\"`\n}\n"
  },
  {
    "path": "routes/aggrtimeout.go",
    "content": "package routes\n\nimport (\n\t\"log\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc parseTimeouts(v string) (int, error) {\n\tvar timeout int\n\tvar err error\n\n\ttimes := map[string]int{\n\t\t\"s\": 1,\n\t\t\"m\": 60,\n\t\t\"h\": 3600,\n\t\t\"d\": 86400,\n\t}\n\n\tv = strings.ReplaceAll(v, \" \", \"\")\n\n\tif v == \"\" {\n\t\treturn 0, nil\n\t}\n\n\twasConvert := false\n\tfor suffix, k := range times {\n\t\tif strings.HasSuffix(strings.ToLower(v), suffix) {\n\t\t\ttimeout, err = strconv.Atoi(strings.TrimSuffix(v, suffix))\n\t\t\ttimeout *= k\n\t\t\twasConvert = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !wasConvert {\n\t\ttimeout, err = strconv.Atoi(v)\n\t}\n\treturn timeout, err\n}\n\nfunc ConfigureTimeouts(route *InputRoute) *InputRoute {\n\taggregateTimeoutSeconds, err := parseTimeouts(route.Plugins.AggregateMessageTimeout)\n\tif err != nil {\n\t\tlog.Printf(\"%q settings: Can't convert 'aggregate-message-timeout'(%q) to seconds.\",\n\t\t\troute.Name, route.Plugins.AggregateMessageTimeout)\n\t}\n\n\troute.Plugins.AggregateTimeoutSeconds = aggregateTimeoutSeconds\n\n\tuniqueMessageTimeoutSeconds, err := parseTimeouts(route.Plugins.UniqueMessageTimeout)\n\tif err != nil {\n\t\tlog.Printf(\"%q settings: Can't convert 'unique-message-timeout'(%q) to seconds.\",\n\t\t\troute.Name, route.Plugins.UniqueMessageTimeout)\n\t}\n\n\troute.Plugins.UniqueMessageTimeoutSeconds = uniqueMessageTimeoutSeconds\n\n\treturn route\n}\n"
  },
  {
    "path": "routes/aggrtimeout_test.go",
    "content": "package routes\n\nimport \"testing\"\n\nvar (\n\ttests = []struct {\n\t\tcaseDesc      string\n\t\ttimeout       string\n\t\texpctdSeconds int\n\t}{\n\t\t{\n\t\t\t\"One minute\",\n\t\t\t\"1m\",\n\t\t\t60,\n\t\t},\n\t\t{\n\t\t\t\"Six hundredths seconds\",\n\t\t\t\"600s\",\n\t\t\t600,\n\t\t},\n\t\t{\n\t\t\t\"Two hours\",\n\t\t\t\"2h\",\n\t\t\t7200,\n\t\t},\n\t\t{\n\t\t\t\"Two days\",\n\t\t\t\"2d\",\n\t\t\t172800,\n\t\t},\n\t\t{\n\t\t\t\"Two days with space between\",\n\t\t\t\"2 d\",\n\t\t\t172800,\n\t\t},\n\t\t{\n\t\t\t\"Exact number of seconds\",\n\t\t\t\"300\",\n\t\t\t300,\n\t\t},\n\t\t{\n\t\t\t\"Invalid format\",\n\t\t\t\"xxxl\",\n\t\t\t0,\n\t\t},\n\t\t{\n\t\t\t\"Empty string\",\n\t\t\t\"\",\n\t\t\t0,\n\t\t},\n\t\t{\n\t\t\t\"a space\",\n\t\t\t\" \",\n\t\t\t0,\n\t\t},\n\t}\n)\n\nfunc TestTimeouts(t *testing.T) {\n\tfor _, test := range tests {\n\t\troute := &InputRoute{}\n\t\troute.Plugins.AggregateMessageTimeout = test.timeout\n\t\troute.Plugins.UniqueMessageTimeout = test.timeout\n\t\troute = ConfigureTimeouts(route)\n\t\tif route.Plugins.AggregateTimeoutSeconds != test.expctdSeconds {\n\t\t\tt.Errorf(\"[%s] Invalid number of seconds in AggregateTimeoutSeconds, expected %d, got %d \\n\", test.caseDesc, test.expctdSeconds, route.Plugins.AggregateTimeoutSeconds)\n\t\t}\n\t\tif route.Plugins.UniqueMessageTimeoutSeconds != test.expctdSeconds {\n\t\t\tt.Errorf(\"[%s] Invalid number of seconds in UniqueMessageTimeout, expected %d, got %d \\n\", test.caseDesc, test.expctdSeconds, route.Plugins.UniqueMessageTimeoutSeconds)\n\t\t}\n\t}\n\n}\n"
  },
  {
    "path": "routes/routes.go",
    "content": "package routes\n\ntype InputRoute struct {\n\tName             string        `json:\"name,omitempty\"`\n\tInput            string        `json:\"input,omitempty\"`\n\tInputFiles       []string      `json:\"input-files,omitempty\"`\n\tActions          []string      `json:\"actions,omitempty\"`\n\tPlugins          Plugins       `json:\"plugins,omitempty\"`\n\tTemplate         string        `json:\"template,omitempty\"`\n\tSerializeActions bool          `json:\"serialize-actions,omitempty\"`\n\tScheduling       chan struct{} `json:\"-\"`\n}\n\ntype Plugins struct {\n\tAggregateMessageNumber      int      `json:\"aggregate-message-number,omitempty\"`\n\tAggregateMessageTimeout     string   `json:\"aggregate-message-timeout,omitempty\"`\n\tAggregateTimeoutSeconds     int      `json:\"aggregate-timeout-seconds,omitempty\"`\n\tUniqueMessageProps          []string `json:\"unique-message-props,omitempty\"`\n\tUniqueMessageTimeout        string   `json:\"unique-message-timeout,omitempty\"`\n\tUniqueMessageTimeoutSeconds int      `json:\"unique-message-timeout-seconds,omitempty\"`\n}\n\nfunc (route *InputRoute) IsSchedulerRun() bool {\n\treturn route.Scheduling != nil\n}\nfunc (route *InputRoute) StartScheduler() {\n\troute.Scheduling = make(chan struct{})\n}\n\nfunc (route *InputRoute) StopScheduler() {\n\tif route.Scheduling != nil {\n\t\tclose(route.Scheduling)\n\t}\n}\n"
  },
  {
    "path": "routes/routes_test.go",
    "content": "package routes\n\nimport (\n\t\"testing\"\n)\n\nfunc TestScheduling(t *testing.T) {\n\tstopCh := make(chan struct{})\n\tdemoRoute1 := &InputRoute{}\n\n\tdemoRoute1Stopped := false\n\tdemoRoute1.StartScheduler()\n\tif !demoRoute1.IsSchedulerRun() {\n\t\tt.Errorf(\"Route 1 is not started\")\n\t}\n\tgo func() {\n\t\t<-demoRoute1.Scheduling\n\t\tdemoRoute1Stopped = true\n\t\tstopCh <- struct{}{}\n\t}()\n\tdemoRoute1.StopScheduler()\n\t<-stopCh\n\n\tif !demoRoute1Stopped {\n\t\tt.Errorf(\"Route 1 is not stopped\")\n\t}\n\n\tdemoRoute2 := &InputRoute{}\n\tif demoRoute2.IsSchedulerRun() {\n\t\tt.Errorf(\"Route 2 should not be started\")\n\t}\n\tdemoRoute2.StopScheduler()\n\n}\n"
  },
  {
    "path": "runner/runner.go",
    "content": "package runner\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/router\"\n\t\"github.com/nats-io/nats.go\"\n)\n\nconst (\n\tNATSConfigSubject = \"postee.config\"\n)\n\ntype Runner struct {\n\tControllerURL      string\n\tRunnerSeedFilePath string\n\tRunnerCARootPath   string\n\tRunnerTLSKeyPath   string\n\tRunnerTLSCertPath  string\n\tRunnerName         string\n}\n\nfunc (r Runner) Setup(rtr *router.Router, cfg *os.File) error {\n\tlog.Println(\"Running in runner mode\")\n\n\tif r.ControllerURL == \"\" {\n\t\treturn fmt.Errorf(\"runner mode requires a valid controller url\")\n\t}\n\n\tvar opts []nats.Option\n\tvar nKeyOpt nats.Option\n\tif r.RunnerSeedFilePath != \"\" {\n\t\tlog.Println(\"Seedfile specified for Runner, enabling AuthN\")\n\t\tvar err error\n\t\tnKeyOpt, err = nats.NkeyOptionFromSeed(r.RunnerSeedFilePath)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to parse seed file: %w\", err)\n\t\t}\n\t\topts = append(opts, nKeyOpt)\n\t}\n\n\tif r.RunnerTLSKeyPath != \"\" && r.RunnerTLSCertPath != \"\" {\n\t\topts = append(opts, nats.ClientCert(r.RunnerTLSCertPath, r.RunnerTLSKeyPath))\n\t\tif r.RunnerCARootPath != \"\" {\n\t\t\topts = append(opts, nats.RootCAs(r.RunnerCARootPath))\n\t\t}\n\t}\n\n\tvar err error\n\trtr.NatsConn, err = nats.Connect(r.ControllerURL, router.SetupConnOptions(opts)...)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to connect to controller at url: %s, err: %w\", r.ControllerURL, err)\n\t}\n\n\tmsg, err := rtr.NatsConn.Request(NATSConfigSubject, []byte(r.RunnerName), time.Second*5)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to obtain runner config from url: %s, err: %w\", r.ControllerURL, err)\n\t}\n\n\tif _, err = cfg.Write(msg.Data); err != nil {\n\t\treturn fmt.Errorf(\"unable to write runner config to disk: %w\", err)\n\t}\n\tlog.Println(\"Runner configuration obtained from: \", r.ControllerURL)\n\n\trtr.ControllerURL = r.ControllerURL\n\trtr.RunnerName = r.RunnerName\n\trtr.Mode = \"runner\"\n\n\treturn nil\n}\n"
  },
  {
    "path": "servicenow/insert_table.go",
    "content": "package servicenow_api\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/aquasecurity/postee/v2/utils\"\n)\n\nfunc InsertRecordToTable(user, password, instance, table string, content []byte) error {\n\turl := fmt.Sprintf(\"https://%s.%s%s%s%s\",\n\t\tinstance, BaseServer, baseApiUrl, tableApi, table)\n\tr := bytes.NewReader(content)\n\tclient := http.DefaultClient\n\treg, err := http.NewRequest(\"POST\", url, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\treg.Header.Add(\"Content-Type\", \"application/json\")\n\treg.Header.Add(\"Authorization\", \"Basic \"+base64.StdEncoding.EncodeToString([]byte(user+\":\"+password)))\n\tresp, err := client.Do(reg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif resp.StatusCode != http.StatusCreated {\n\t\treturn fmt.Errorf(\"InsertRecordToTable Error: %v\\nHeader: %v\",\n\t\t\tresp.Status, utils.PrnLogResponse(resp.Body))\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "servicenow/servicenow_base.go",
    "content": "package servicenow_api\n\nconst (\n\tBaseServer = \"service-now.com/\"\n\tbaseApiUrl = \"api/now/\"\n\ttableApi   = \"table/\"\n)\n\ntype ServiceNowData struct {\n\tShortDescription string `json:\"short_description\"`\n\tWorkNotes        string `json:\"work_notes\"`\n\tOpened           string `json:\"opened_at\"`\n\tCaller           string `json:\"caller_id\"`\n\tCategory         string `json:\"category\"`\n\tSubcategory      string `json:\"subcategory\"`\n\tImpact           int    `json:\"impact\"`\n\tUrgency          int    `json:\"urgency\"`\n\tState            int    `json:\"state\"`\n\tDescription      string `json:\"description\"`\n\tAssignedTo       string `json:\"assigned_to\"`\n\tAssignmentGroup  string `json:\"assignment_group\"`\n}\n"
  },
  {
    "path": "slack/sendtoslack.go",
    "content": "package slack_api\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc SendToUrl(url string, data []byte) error {\n\tr := bytes.NewReader(data)\n\tresp, err := http.Post(url, \"application/json\", r)\n\tif err != nil {\n\t\tlog.Printf(\"Slack API error: %v\", err)\n\t\treturn err\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\tdefer resp.Body.Close()\n\t\tmsg, err := ioutil.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn fmt.Errorf(\"Slack API error: Status: %q. Message: %q\",\n\t\t\tresp.Status, msg)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "teams/teams_requests.go",
    "content": "package teams_api\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\n\t\"github.com/aquasecurity/postee/v2/utils\"\n)\n\nfunc CreateMessageByWebhook(webhook, content string) error {\n\tvar message bytes.Buffer\n\tfmt.Fprintf(&message, \"{\\\"text\\\":\\\"%s\\\"}\", content)\n\n\tutils.Debug(\"Data for sending to %q: %q\\n\", webhook, message.String())\n\tr := bytes.NewReader(message.Bytes())\n\tclient := http.DefaultClient\n\treg, err := http.NewRequest(\"POST\", webhook, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\treg.Header.Add(\"Content-Type\", \"application/json\")\n\tresp, err := client.Do(reg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer resp.Body.Close()\n\tif message, _ := ioutil.ReadAll(resp.Body); resp.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"InsertRecordToTable Error: %q. %s\", resp.Status, message)\n\t} else {\n\t\tif message[0] != '1' {\n\t\t\treturn fmt.Errorf(\"Teams Body Error: %q\", string(message))\n\t\t}\n\t\tutils.Debug(\"Response body: %q\\n\", message)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "ui/backend/dbservice/getplgnstats.go",
    "content": "package dbservice\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\n\thookDbService \"github.com/aquasecurity/postee/v2/dbservice\"\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc GetPlgnStats() (r map[string]int, err error) {\n\tr = make(map[string]int)\n\n\tvar DbPath string\n\tif len(os.Getenv(\"PATH_TO_DB\")) > 0 {\n\t\tDbPath = os.Getenv(\"PATH_TO_DB\")\n\t} else {\n\t\tDbPath = hookDbService.DbPath\n\t}\n\n\tdb, err := bolt.Open(DbPath, 0444, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer db.Close()\n\terr = db.View(func(tx *bolt.Tx) error {\n\t\tbucket := tx.Bucket([]byte(hookDbService.DbBucketActionStats))\n\t\tif bucket == nil {\n\t\t\treturn nil //no bucket - empty stats will be returned\n\t\t}\n\n\t\tc := bucket.Cursor()\n\n\t\tfor k, v := c.First(); k != nil; k, v = c.Next() {\n\t\t\tcnt, err := strconv.Atoi(string(v[:]))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tr[string(k[:])] = cnt\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn r, nil\n}\n"
  },
  {
    "path": "ui/backend/go.mod",
    "content": "module github.com/aquasecurity/postee/ui/backend\n\ngo 1.18\n\nrequire (\n\tgithub.com/aquasecurity/postee/v2 v2.6.0\n\tgithub.com/gorilla/mux v1.8.0\n\tgithub.com/gorilla/securecookie v1.1.1\n\tgithub.com/gorilla/sessions v1.2.1\n\tgithub.com/stretchr/testify v1.8.0\n\tgo.etcd.io/bbolt v1.3.6\n)\n\nrequire (\n\tgithub.com/Microsoft/go-winio v0.5.1 // indirect\n\tgithub.com/OneOfOne/xxhash v1.2.8 // indirect\n\tgithub.com/agnivade/levenshtein v1.1.1 // indirect\n\tgithub.com/aquasecurity/go-jira v0.0.0-20211103111421-b62ce48827be // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/docker/distribution v2.8.2+incompatible // indirect\n\tgithub.com/docker/docker v20.10.24+incompatible // indirect\n\tgithub.com/docker/go-connections v0.4.0 // indirect\n\tgithub.com/docker/go-units v0.4.0 // indirect\n\tgithub.com/fatih/structs v1.1.0 // indirect\n\tgithub.com/ghodss/yaml v1.0.0 // indirect\n\tgithub.com/go-logr/logr v1.2.3 // indirect\n\tgithub.com/gobwas/glob v0.2.3 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/golang-jwt/jwt/v4 v4.2.0 // indirect\n\tgithub.com/golang/protobuf v1.5.2 // indirect\n\tgithub.com/google/go-cmp v0.5.8 // indirect\n\tgithub.com/google/go-querystring v1.1.0 // indirect\n\tgithub.com/google/gofuzz v1.2.0 // indirect\n\tgithub.com/google/uuid v1.2.0 // indirect\n\tgithub.com/googleapis/gnostic v0.5.5 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-retryablehttp v0.5.3 // indirect\n\tgithub.com/imdario/mergo v0.3.12 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/compress v1.14.4 // indirect\n\tgithub.com/minio/highwayhash v1.0.2 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 // indirect\n\tgithub.com/nats-io/nats-server/v2 v2.7.4 // indirect\n\tgithub.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d // indirect\n\tgithub.com/nats-io/nkeys v0.3.0 // indirect\n\tgithub.com/nats-io/nuid v1.0.1 // indirect\n\tgithub.com/open-policy-agent/opa v0.44.0 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect\n\tgithub.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.0 // indirect\n\tgithub.com/spf13/pflag v1.0.5 // indirect\n\tgithub.com/tchap/go-patricia/v2 v2.3.1 // indirect\n\tgithub.com/tidwall/gjson v1.14.0 // indirect\n\tgithub.com/tidwall/match v1.1.1 // indirect\n\tgithub.com/tidwall/pretty v1.2.0 // indirect\n\tgithub.com/trivago/tgo v1.0.7 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/yashtewari/glob-intersection v0.1.0 // indirect\n\tgolang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect\n\tgolang.org/x/net v0.7.0 // indirect\n\tgolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect\n\tgolang.org/x/sys v0.5.0 // indirect\n\tgolang.org/x/term v0.5.0 // indirect\n\tgolang.org/x/text v0.7.0 // indirect\n\tgolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect\n\tgoogle.golang.org/appengine v1.6.7 // indirect\n\tgoogle.golang.org/protobuf v1.28.1 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/yaml.v2 v2.4.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n\tk8s.io/api v0.23.3 // indirect\n\tk8s.io/apimachinery v0.23.3 // indirect\n\tk8s.io/client-go v0.23.3 // indirect\n\tk8s.io/klog/v2 v2.30.0 // indirect\n\tk8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect\n\tk8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect\n\tsigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect\n\tsigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect\n\tsigs.k8s.io/yaml v1.3.0 // indirect\n)\n\nreplace (\n\tgithub.com/containerd/containerd v1.6.2 => github.com/containerd/containerd v1.6.6\n\tgolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 => golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f\n)\n"
  },
  {
    "path": "ui/backend/go.sum",
    "content": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\ngithub.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=\ngithub.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=\ngithub.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA=\ngithub.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M=\ngithub.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=\ngithub.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=\ngithub.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=\ngithub.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY=\ngithub.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84=\ngithub.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=\ngithub.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=\ngithub.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=\ngithub.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=\ngithub.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=\ngithub.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=\ngithub.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=\ngithub.com/aquasecurity/go-jira v0.0.0-20211103111421-b62ce48827be h1:xUasZnauNAn2jY0gfVG+Ro371S31s3SfVUvcjhwIMyI=\ngithub.com/aquasecurity/go-jira v0.0.0-20211103111421-b62ce48827be/go.mod h1:IHtKzIAdk0t3Xse7rJSY7pJlA8gB7lqY2b4l5WYZYsk=\ngithub.com/aquasecurity/postee/v2 v2.6.0 h1:3UH5b7LlGLSRMMAMvjCYUwYGbkV1Wa1TQ+nlG7eoO2Y=\ngithub.com/aquasecurity/postee/v2 v2.6.0/go.mod h1:mAE5BlBooI8z3nWHldckWcCwP1smfIVwPSLtvSAujzM=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q=\ngithub.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=\ngithub.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/bytecodealliance/wasmtime-go v0.36.0 h1:B6thr7RMM9xQmouBtUqm1RpkJjuLS37m6nxX+iwsQSc=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=\ngithub.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8=\ngithub.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI=\ngithub.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=\ngithub.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=\ngithub.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=\ngithub.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE=\ngithub.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=\ngithub.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=\ngithub.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=\ngithub.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=\ngithub.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=\ngithub.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=\ngithub.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=\ngithub.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=\ngithub.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=\ngithub.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=\ngithub.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=\ngithub.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=\ngithub.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg=\ngithub.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=\ngithub.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=\ngithub.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0=\ngithub.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=\ngithub.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=\ngithub.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=\ngithub.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=\ngithub.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=\ngithub.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=\ngithub.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=\ngithub.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU=\ngithub.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw=\ngithub.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA=\ngithub.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=\ngithub.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=\ngithub.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-retryablehttp v0.5.1/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=\ngithub.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=\ngithub.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4=\ngithub.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI=\ngithub.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg=\ngithub.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=\ngithub.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c=\ngithub.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY=\ngithub.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k=\ngithub.com/nats-io/nats-server/v2 v2.7.4 h1:c+BZJ3rGzUKCBIM4IXO8uNT2u1vajGbD1kPA6wqCEaM=\ngithub.com/nats-io/nats-server/v2 v2.7.4/go.mod h1:1vZ2Nijh8tcyNe8BDVyTviCd9NYzRbubQYiEHsvOQWc=\ngithub.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d h1:zJf4l8Kp67RIZhoVeniSLZs69SHNgjLHz0aNsqPPlx8=\ngithub.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w=\ngithub.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8=\ngithub.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4=\ngithub.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=\ngithub.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=\ngithub.com/open-policy-agent/opa v0.44.0 h1:sEZthsrWBqIN+ShTMJ0Hcz6a3GkYsY4FaB2S/ou2hZk=\ngithub.com/open-policy-agent/opa v0.44.0/go.mod h1:YpJaFIk5pq89n/k72c1lVvfvR5uopdJft2tMg1CW/yU=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec=\ngithub.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=\ngithub.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10 h1:qHnitdkr8TN/irubnQM8ml/udTyAxo6j5v61H7+TV3k=\ngithub.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10/go.mod h1:4OjcxgwdXzezqytxN534MooNmrxRD50geWZxTD7845s=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=\ngithub.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE=\ngithub.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=\ngithub.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ=\ngithub.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=\ngithub.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=\ngithub.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes=\ngithub.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=\ngithub.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=\ngithub.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=\ngithub.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=\ngithub.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg=\ngithub.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngo.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU=\ngo.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=\ngolang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=\ngolang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=\ngolang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=\ngoogle.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.23.3 h1:KNrME8KHGr12Ozjf8ytOewKzZh6hl/hHUZeHddT3a38=\nk8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ=\nk8s.io/apimachinery v0.23.3 h1:7IW6jxNzrXTsP0c8yXz2E5Yx/WTzVPTsHIx/2Vm0cIk=\nk8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM=\nk8s.io/client-go v0.23.3 h1:23QYUmCQ/W6hW78xIwm3XqZrrKZM+LWDqW2zfo+szJs=\nk8s.io/client-go v0.23.3/go.mod h1:47oMd+YvAOqZM7pcQ6neJtBiFH7alOyfunYN48VsmwE=\nk8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=\nk8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=\nk8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=\nk8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw=\nk8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=\nk8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4=\nk8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk=\nk8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nk8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE=\nk8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s=\nsigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs=\nsigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=\nsigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y=\nsigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4=\nsigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=\nsigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=\nsigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=\n"
  },
  {
    "path": "ui/backend/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/aquasecurity/postee/ui/backend/uiserver\"\n)\n\nconst (\n\tENV_FILELOG        = \"POSTEE_UI_LOGFILE\"\n\tENV_CFG            = \"POSTEE_UI_CFG\"\n\tENV_WEB            = \"POSTEE_UI_WEB\"\n\tENV_UPDATE_URL     = \"POSTEE_UI_UPDATE_URL\"\n\tENV_PORT           = \"POSTEE_UI_PORT\"\n\tENV_ADMIN_USER     = \"POSTEE_ADMIN_USER\"\n\tENV_ADMIN_PASSWORD = \"POSTEE_ADMIN_PASSWORD\"\n\n\tDEFAULT_WEB_PATH = \"/uiserver/www\"\n)\n\nfunc main() {\n\tlogfile := os.Getenv(ENV_FILELOG)\n\tif logfile != \"\" {\n\t\tf, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0444)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tdefer f.Close()\n\t\tlog.SetOutput(f)\n\t}\n\n\tcfg := os.Getenv(ENV_CFG)\n\tif cfg == \"\" {\n\t\tlog.Fatalf(\"cfg file name is empty. You have to set a filename via %q environment variable.\", ENV_CFG)\n\t}\n\tweb := os.Getenv(ENV_WEB)\n\tif web == \"\" {\n\t\tweb = DEFAULT_WEB_PATH\n\t\tlog.Printf(\"The default path to web (%q) is using now.\", web)\n\t}\n\tupdateUrl := os.Getenv(ENV_UPDATE_URL)\n\tif updateUrl == \"\" {\n\t\tlog.Printf(\"WARNING! Using an empty update url, UI won't restart your Postee instance with a saved configuration. You can change it via %q environment variable.\", ENV_UPDATE_URL)\n\t}\n\n\tport := os.Getenv(ENV_PORT)\n\tif port == \"\" {\n\t\tport = \"8090\"\n\t\tlog.Printf(\"WARNING! Using a default port: %s. You can change it via %q environment variable.\", port, ENV_PORT)\n\t}\n\n\tadmusr := os.Getenv(ENV_ADMIN_USER)\n\tif admusr == \"\" {\n\t\tadmusr = \"admin\"\n\t\tlog.Printf(\"WARNING! Using a default admin user. You can change it via %q environment variable.\", ENV_ADMIN_USER)\n\t}\n\n\tadmpwd := os.Getenv(ENV_ADMIN_PASSWORD)\n\tif admpwd == \"\" {\n\t\tadmpwd = \"admin\"\n\t\tlog.Printf(\"WARNING! Using a default admin password. You can change it via %q environment variable.\", ENV_ADMIN_PASSWORD)\n\t}\n\n\tserver := uiserver.Instance(web, port, cfg, updateUrl, admusr, admpwd)\n\tserver.Start()\n\tdefer server.Stop()\n}\n"
  },
  {
    "path": "ui/backend/uiserver/authentication.go",
    "content": "package uiserver\n\nimport (\n\t\"net/http\"\n)\n\nconst (\n\tsessioncookiename = \"postee-session-cookie\"\n)\n\nfunc (srv *uiServer) login(w http.ResponseWriter, r *http.Request) {\n\tdefer r.Body.Close()\n\tsession, err := srv.store.Get(r, sessioncookiename)\n\tif err != nil {\n\t\tsession, err = srv.store.New(r, sessioncookiename)\n\t}\n\n\tif session.Values[\"user\"] == nil {\n\t\tfrmusr := r.FormValue(\"username\")\n\t\tfrmpwd := r.FormValue(\"password\")\n\n\t\tif frmusr == srv.admusr && frmpwd == srv.admpwd {\n\t\t\tsession.Values[\"user\"] = frmusr\n\t\t\terr = session.Save(r, w)\n\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t}\n\t}\n\n}\n\nfunc (srv *uiServer) logout(w http.ResponseWriter, r *http.Request) {\n\tsession, err := srv.store.Get(r, sessioncookiename)\n\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tsession.Values[\"user\"] = \"\"\n\tsession.Options.MaxAge = -1\n\n\terr = session.Save(r, w)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "ui/backend/uiserver/authentication_middleware.go",
    "content": "package uiserver\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n)\n\nfunc (srv *uiServer) authenticationMiddleware(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\n\t\tif !strings.HasPrefix(r.RequestURI, \"/api\") {\n\t\t\tnext.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\n\t\tif strings.HasPrefix(r.RequestURI, \"/api/login\") {\n\t\t\tnext.ServeHTTP(w, r)\n\t\t\treturn\n\t\t}\n\n\t\tif user, err := srv.getUserFromRequest(r); err == nil && user != \"\" {\n\t\t\tnext.ServeHTTP(w, r)\n\t\t} else {\n\t\t\t// Write an error and stop the handler chain\n\t\t\thttp.Error(w, \"Forbidden\", http.StatusUnauthorized)\n\t\t}\n\t})\n}\n\nfunc (srv *uiServer) getUserFromRequest(r *http.Request) (string, error) {\n\tsession, err := srv.store.Get(r, sessioncookiename)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tuserObj := session.Values[\"user\"]\n\tif userObj == nil {\n\t\treturn \"\", nil\n\t} else {\n\t\treturn userObj.(string), nil\n\t}\n}\n"
  },
  {
    "path": "ui/backend/uiserver/config.go",
    "content": "package uiserver\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\n\thookDbService \"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/router\"\n)\n\nfunc (srv *uiServer) getConfig(w http.ResponseWriter, r *http.Request) {\n\tlog.Printf(\"configured config path %s\", srv.cfgPath)\n\n\t_, err := router.Parsev2cfg(srv.cfgPath)\n\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tw.Write([]byte(fmt.Sprintf(\"Invalid config file format: %s\", err.Error())))\n\t\treturn\n\t}\n\n\td, err := ioutil.ReadFile(srv.cfgPath)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tw.Write([]byte(err.Error()))\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"text/yaml\")\n\tw.WriteHeader(http.StatusOK)\n\tw.Write(d)\n}\n\nfunc (srv *uiServer) updateConfig(w http.ResponseWriter, r *http.Request) {\n\tdefer r.Body.Close()\n\tinputYaml, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\thttp.Error(w, \"Can't read JSON string\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tif err := os.Rename(srv.cfgPath, srv.cfgPath+\".copy\"); err != nil {\n\t\tlog.Printf(\"rename file error %v\", err)\n\t\thttp.Error(w, \"Can't remove data from the config file for overwrite\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tf, err := os.Create(srv.cfgPath)\n\tif err != nil {\n\t\tlog.Printf(\"create file error %v\", err)\n\t\thttp.Error(w, \"Can't open the config file for overwrite\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tdefer f.Close()\n\t_, err = f.Write(inputYaml)\n\tif err != nil {\n\t\tlog.Printf(\"write file error %v\", err)\n\t\thttp.Error(w, \"Can't write to the config file for overwrite\", http.StatusBadRequest)\n\t\treturn\n\t}\n\tos.RemoveAll(srv.cfgPath + \".copy\")\n\n\tapikey, err := hookDbService.GetApiKey()\n\n\tif err != nil {\n\t\tlog.Printf(\"Can not load api key from bolt %v\", err)\n\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\treturn\n\t}\n\n\terr = reloadWebhookCfg(srv.webhookUrl, apikey)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to reach Postee backend %v\", err)\n\t\thttp.Error(w, \"Unable to reach Postee backend\", http.StatusBadRequest)\n\t\treturn\n\t}\n}\n\nfunc reloadWebhookCfg(url string, key string) error {\n\tu := fmt.Sprintf(\"%s/reload?key=%s\", url, key)\n\tresp, err := http.Get(u)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\treturn nil\n}\n"
  },
  {
    "path": "ui/backend/uiserver/events.go",
    "content": "package uiserver\n\nimport (\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n)\n\nfunc (srv *uiServer) getEvents(w http.ResponseWriter, r *http.Request) {\n\tlog.Printf(\"configured config path %s\", srv.cfgPath)\n\n\tposteeUrl := os.Getenv(\"POSTEE_UI_UPDATE_URL\")\n\tif len(posteeUrl) <= 0 {\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\tlog.Println(\"No Postee URL configured, set POSTEE_UI_UPDATE_URL to the Postee URL\")\n\t\treturn\n\t}\n\n\tresp, err := http.Get(posteeUrl + \"/events\")\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tlog.Println(\"Unable to reach Postee at URL: \" + posteeUrl + \"/events\" + \" err: \" + err.Error())\n\t\treturn\n\t}\n\n\tcurrentEvents, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\tlog.Println(\"Failed to read events: \" + err.Error())\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", \"text/json\")\n\tw.WriteHeader(http.StatusOK)\n\t_, _ = w.Write(currentEvents)\n}\n"
  },
  {
    "path": "ui/backend/uiserver/events_test.go",
    "content": "package uiserver\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUiServer_getEvents(t *testing.T) {\n\ttestCases := []struct {\n\t\tname               string\n\t\ttsHandlerFunc      http.HandlerFunc\n\t\texpectedResp       string\n\t\texpectedStatusCode int\n\t}{\n\t\t{\n\t\t\tname: \"happy path\",\n\t\t\ttsHandlerFunc: func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t_, _ = w.Write([]byte(`[\n   {\n      \"SigMetadata\":{\n         \"ID\":\"TRC-2\",\n         \"hostname\":\"postee-0\"\n      }\n   },\n   {\n      \"SigMetadata\":{\n         \"ID\":\"TRC-3\",\n         \"hostname\":\"postee-0\"\n      }\n   }\n]`))\n\t\t\t},\n\t\t\texpectedResp: `[\n   {\n      \"SigMetadata\":{\n         \"ID\":\"TRC-2\",\n         \"hostname\":\"postee-0\"\n      }\n   },\n   {\n      \"SigMetadata\":{\n         \"ID\":\"TRC-3\",\n         \"hostname\":\"postee-0\"\n      }\n   }\n]`,\n\t\t\texpectedStatusCode: http.StatusOK,\n\t\t},\n\t\t{\n\t\t\tname:               \"sad path, no postee url set\",\n\t\t\texpectedStatusCode: http.StatusBadRequest,\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tif tc.tsHandlerFunc != nil {\n\t\t\t\tts := httptest.NewServer(tc.tsHandlerFunc)\n\t\t\t\tdefer ts.Close()\n\n\t\t\t\trequire.NoError(t, os.Setenv(\"POSTEE_UI_UPDATE_URL\", ts.URL))\n\t\t\t\tdefer func() {\n\t\t\t\t\t_ = os.Unsetenv(\"POSTEE_UI_UPDATE_URL\")\n\t\t\t\t}()\n\t\t\t}\n\n\t\t\tw := httptest.NewRecorder()\n\t\t\tvar r *http.Request\n\t\t\tsrv := uiServer{}\n\t\t\tsrv.getEvents(w, r)\n\n\t\t\tresp := w.Result()\n\t\t\tdefer func() {\n\t\t\t\t_ = resp.Body.Close()\n\t\t\t}()\n\t\t\tgot, _ := ioutil.ReadAll(resp.Body)\n\t\t\tassert.Equal(t, tc.expectedStatusCode, resp.StatusCode, tc.name)\n\t\t\tif tc.tsHandlerFunc != nil {\n\t\t\t\tassert.JSONEq(t, tc.expectedResp, string(got), tc.name)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, tc.expectedResp, string(got), tc.name)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "ui/backend/uiserver/httpserver.go",
    "content": "package uiserver\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n)\n\ntype localWebServer struct {\n\tlocalPath string\n\turl       string\n}\n\nfunc (web *localWebServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tpath, err := filepath.Abs(r.URL.Path)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\tpath = filepath.Join(web.localPath, path)\n\t_, err = os.Stat(path)\n\tif os.IsNotExist(err) {\n\t\thttp.ServeFile(w, r, filepath.Join(web.localPath, web.url))\n\t\treturn\n\t} else if err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\thttp.FileServer(http.Dir(web.localPath)).ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "ui/backend/uiserver/plgnstats.go",
    "content": "package uiserver\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/aquasecurity/postee/ui/backend/dbservice\"\n)\n\nfunc (srv *uiServer) plgnStats(w http.ResponseWriter, r *http.Request) {\n\tstats, err := dbservice.GetPlgnStats()\n\tif err != nil {\n\t\thandleErr(w, err)\n\t\treturn\n\t}\n\tdata, err := json.Marshal(stats)\n\tif err != nil {\n\t\thandleErr(w, err)\n\t\treturn\n\t}\n\n\tw.WriteHeader(http.StatusOK)\n\tw.Write([]byte(data))\n}\nfunc handleErr(w http.ResponseWriter, err error) {\n\tw.WriteHeader(http.StatusInternalServerError)\n\tw.Write([]byte(err.Error()))\n}\n"
  },
  {
    "path": "ui/backend/uiserver/server.go",
    "content": "package uiserver\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/gorilla/mux\"\n\t\"github.com/gorilla/securecookie\"\n\t\"github.com/gorilla/sessions\"\n)\n\ntype uiServer struct {\n\tport       string\n\tcfgPath    string\n\tboltDbPath string\n\twebhookUrl string\n\tupdateKey  string\n\tadmusr     string\n\tadmpwd     string\n\trouter     *mux.Router\n\tstore      *sessions.CookieStore\n}\n\nfunc Instance(webLocalPath, port, cfg, webhookUrl, admusr string, admpwd string) *uiServer {\n\tserver := &uiServer{\n\t\tport:       port,\n\t\tcfgPath:    cfg,\n\t\twebhookUrl: webhookUrl,\n\t\tadmusr:     admusr,\n\t\tadmpwd:     admpwd,\n\t\trouter:     mux.NewRouter().StrictSlash(true),\n\t}\n\tauthKeyOne := securecookie.GenerateRandomKey(64)\n\tencryptionKeyOne := securecookie.GenerateRandomKey(32)\n\n\tserver.store = sessions.NewCookieStore(\n\t\tauthKeyOne,\n\t\tencryptionKeyOne,\n\t)\n\n\tserver.store.Options = &sessions.Options{\n\t\tMaxAge:   60 * 60 * 24, //one day\n\t\tHttpOnly: true,\n\t}\n\n\tserver.router.Use(server.authenticationMiddleware)\n\n\tserver.router.HandleFunc(\"/api/login\", server.login).Methods(\"POST\")\n\tserver.router.HandleFunc(\"/api/logout\", server.logout).Methods(\"GET\")\n\tserver.router.HandleFunc(\"/api/config\", server.updateConfig).Methods(\"POST\")\n\tserver.router.HandleFunc(\"/api/config\", server.getConfig).Methods(\"GET\")\n\tserver.router.HandleFunc(\"/api/test\", server.testSettings).Methods(\"POST\")\n\tserver.router.HandleFunc(\"/api/actions/stats\", server.plgnStats).Methods(\"GET\")\n\tserver.router.HandleFunc(\"/api/events\", server.getEvents).Methods(\"GET\")\n\n\tserver.router.HandleFunc(\"/ping\", server.pingHandler).Methods(\"GET\")\n\n\tweb := &localWebServer{\n\t\tlocalPath: webLocalPath,\n\t\turl:       \"/\",\n\t}\n\tserver.router.PathPrefix(\"/\").Handler(web)\n\treturn server\n}\n\nfunc (srv *uiServer) Start() {\n\tlog.Print(\"UI Postee server starting...\")\n\thttp.ListenAndServe(\":\"+srv.port, srv.router)\n}\n\nfunc (srv *uiServer) Stop() {\n\tlog.Print(\"UI Postee server stopped!\")\n}\n\nfunc (ctx *uiServer) pingHandler(w http.ResponseWriter, r *http.Request) {\n\tw.WriteHeader(http.StatusOK)\n}\n"
  },
  {
    "path": "ui/backend/uiserver/testplg.go",
    "content": "package uiserver\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\t\"github.com/aquasecurity/postee/v2/router\"\n)\n\nfunc (srv *uiServer) testSettings(w http.ResponseWriter, r *http.Request) {\n\tplgSettings := &router.ActionSettings{}\n\n\tdefer r.Body.Close()\n\tbody, err := io.ReadAll(r.Body)\n\n\tif err != nil {\n\t\thttp.Error(w, \"Invalid request\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif err := json.Unmarshal(body, plgSettings); err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"Can't read JSON string %s\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tplg := router.BuildAndInitOtpt(plgSettings, \"\")\n\n\ttestPayload := make(map[string]string)\n\n\ttestPayload[\"title\"] = \"Postee test title\"\n\ttestPayload[\"description\"] = layout.GenTestDescription(plg.GetLayoutProvider(), \"Postee test description\")\n\n\tlog.Printf(\"description is: %s \\n\", testPayload[\"description\"])\n\n\terr = plg.Send(testPayload)\n\n\tif err != nil {\n\t\t//TODO provide method to write error response as JSON\n\t\thttp.Error(w, fmt.Sprintf(\"Can't test output: %s \\n\", err), http.StatusBadRequest)\n\t\treturn\n\t}\n\n}\n"
  },
  {
    "path": "ui/backend/uiserver/update_test.go",
    "content": "package uiserver\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n)\n\nconst (\n\ttestCfgFile = \"test.cfg\"\n\n\tinputConfigJson = `[{\"type\":\"common\"}]`\n)\n\nfunc TestUpdateConfig(t *testing.T) {\n\ttests := []struct {\n\t\tinput  string\n\t\tstatus int\n\t}{\n\t\t{inputConfigJson, http.StatusOK},\n\t}\n\n\tsrv := &uiServer{\n\t\tcfgPath: testCfgFile,\n\t}\n\tos.Create(testCfgFile)\n\tdefer os.RemoveAll(testCfgFile)\n\n\tfor _, test := range tests {\n\t\treq := httptest.NewRequest(\"POST\", \"/update\", strings.NewReader(test.input))\n\t\tw := httptest.NewRecorder()\n\t\tsrv.updateConfig(w, req)\n\t\tresponse := w.Result()\n\n\t\tmsg, err := io.ReadAll(response.Body)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tif st := w.Result().StatusCode; st != test.status {\n\t\t\tt.Errorf(\"request to /update returns a wrong status %d, wanted %d.\\nData: %q\\nMessage: %q\", st, test.status, test.input, string(msg))\n\t\t}\n\t\tresponse.Body.Close()\n\t}\n}\n"
  },
  {
    "path": "ui/frontend/.gitignore",
    "content": ".DS_Store\nnode_modules\n/dist\n\n\n# local env files\n.env.local\n.env.*.local\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# Editor directories and files\n.idea\n.vscode\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "ui/frontend/README.md",
    "content": "# postee-ui\n\n## Project setup\n```\nyarn install\n```\n\n### Compiles and hot-reloads for development\n```\nyarn serve\n```\n\n### Compiles and minifies for production\n```\nyarn build\n```\n\n### Lints and fixes files\n```\nyarn lint\n```\n\n### Customize configuration\nSee [Configuration Reference](https://cli.vuejs.org/config/).\n"
  },
  {
    "path": "ui/frontend/babel.config.js",
    "content": "module.exports = {\n  presets: [\n    '@vue/cli-plugin-babel/preset'\n  ]\n}\n"
  },
  {
    "path": "ui/frontend/package.json",
    "content": "{\n  \"name\": \"postee-ui\",\n  \"version\": \"0.2.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"serve\": \"vue-cli-service serve\",\n    \"build\": \"vue-cli-service build\",\n    \"lint\": \"vue-cli-service lint\"\n  },\n  \"dependencies\": {\n    \"axios\": \"^0.21.1\",\n    \"bootstrap-vue\": \"^2.21.2\",\n    \"codemirror-rego\": \"^1.1.0\",\n    \"core-js\": \"^3.18.1\",\n    \"js-yaml\": \"^4.1.0\",\n    \"node-forge\": \"^1.3.0\",\n    \"vue\": \"^2.6.11\",\n    \"vue-codemirror\": \"^4.0.6\",\n    \"vue-json-pretty\": \"^1.8.3\",\n    \"vue-json-viewer\": \"2\",\n    \"vue-router\": \"^3.5.1\",\n    \"vue-tmx\": \"^0.1.12\",\n    \"vue-tour\": \"^2.0.0\",\n    \"vuex\": \"^3.6.2\"\n  },\n  \"devDependencies\": {\n    \"@vue/cli-plugin-babel\": \"^5.0.6\",\n    \"@vue/cli-plugin-eslint\": \"^5.0.6\",\n    \"@vue/cli-service\": \"^5.0.8\",\n    \"babel-eslint\": \"^10.1.0\",\n    \"eslint\": \"^7.32.0\",\n    \"eslint-plugin-vue\": \"^7.18.0\",\n    \"vue-template-compiler\": \"^2.6.11\"\n  },\n  \"resolutions\": {\n    \"ansi-regex\": \"5.0.1\",\n    \"glob-parent\": \"5.1.2\",\n    \"nth-check\": \"2.0.1\",\n    \"node-forge\": \"1.3.0\"\n  },\n  \"eslintConfig\": {\n    \"root\": true,\n    \"env\": {\n      \"node\": true\n    },\n    \"extends\": [\n      \"plugin:vue/essential\",\n      \"eslint:recommended\"\n    ],\n    \"parserOptions\": {\n      \"parser\": \"babel-eslint\"\n    },\n    \"rules\": {}\n  },\n  \"browserslist\": [\n    \"> 1%\",\n    \"last 2 versions\",\n    \"not dead\"\n  ]\n}\n"
  },
  {
    "path": "ui/frontend/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width,initial-scale=1.0\">\n    <link rel=\"icon\" href=\"<%= BASE_URL %>favicon.png\">\n    <title><%= htmlWebpackPlugin.options.title %></title>\n\n  </head>\n  <body>\n    <noscript>\n      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>\n    </noscript>\n    <div id=\"app\"></div>\n    <!-- built files will be auto injected -->\n  </body>\n</html>\n"
  },
  {
    "path": "ui/frontend/src/App.vue",
    "content": "<template>\n\n  <div id=\"app\">\n    <b-navbar toggleable=\"lg\" type=\"dark\" variant=\"primary\" class=\"navbar-expand-lg mb-3\">\n      <b-navbar-brand href=\"#\">Postee</b-navbar-brand>\n    </b-navbar>\n\n    <div>\n      <v-tour name=\"myTour\" :callbacks=\"tourCallbacks\" :steps=\"steps\" :options=\"{ highlight: false }\"></v-tour>\n    </div>\n\n    <div class=\"container-fluid\">\n      <div class=\"row content\">\n        <div v-if=\"!isOnLogin\" class=\"col-sm-2\">\n          <ul class=\"nav flex-column nav-pills\">\n            <li class=\"nav-item\">\n              <router-link\n                  active-class=\"active\"\n                  :to=\"{ name: 'routes' }\"\n                  class=\"nav-link\"\n                  id=\"routes\"\n              >Routes</router-link\n              >\n            </li>\n            <li class=\"nav-item\">\n              <router-link\n                active-class=\"active\"\n                :to=\"{ name: 'actions' }\"\n                class=\"nav-link\"\n                >Actions</router-link\n              >\n            </li>\n            <li class=\"nav-item\">\n              <router-link\n                active-class=\"active\"\n                :to=\"{ name: 'templates' }\"\n                class=\"nav-link\"\n                >Templates</router-link\n              >\n            </li>\n            <li class=\"nav-item\">\n              <router-link\n                active-class=\"active\"\n                :to=\"{ name: 'settings' }\"\n                class=\"nav-link\"\n                >Settings</router-link\n              >\n            </li>\n            <li class=\"nav-item\">\n              <router-link\n                  active-class=\"active\"\n                  :to=\"{ name: 'events' }\"\n                  class=\"nav-link\"\n                  id=\"events\"\n                >Events</router-link\n              >\n            </li>\n            <li class=\"nav-item\">\n              <a class=\"nav-link\" @click=\"doLogout\" href=\"#\">Logout</a>\n            </li>\n          </ul>\n        </div>\n        <div\n          v-bind:class=\"[\n            { 'col-sm-12': isOnLogin },\n            { 'col-sm-9': !isOnLogin },\n          ]\"\n        >\n          <div v-if=\"error\" class=\"alert alert-danger\" role=\"alert\">\n            {{ error }}\n          </div>\n          <router-view></router-view>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n<style>\n</style>\n<script>\nimport { mapState } from \"vuex\";\n\nexport default {\n  name: \"App\",\n  watch: {\n    $route(to) {\n      this.$store.commit(\"error/clear\");\n      if ([\"home\", \"routes\"].indexOf(to.name) >= 0 && !this.$store.state.flags.all.loaded) {\n        this.startLoading();\n      }\n    },\n  },\n  data() {\n    return {\n      tourCallbacks: {\n        onStop: this.stopTour,\n      },\n      steps: [\n        {\n          target: '#routes',\n          content: \"Routes are scenarios that can occur based on input events\",\n          params: {\n            enableScrolling: false,\n          },\n        },\n        {\n          target: \"#add-route\",\n          content: \"Let's begin by adding a new route\",\n          params: {\n            enableScrolling: false,\n          },\n        },\n        {\n          target: \"#name\",\n          content: \"Start by giving it a name\",\n          params: {\n            enableScrolling: false,\n            placement: 'left',\n          }\n        },\n        {\n          target: \"#select-input-policies\",\n          content: \"Policies define when a route will be triggered\",\n          params: {\n            enableScrolling: false,\n            placement: 'left',\n          }\n        },\n        {\n          target: \"#actions\",\n          content: \"When a policy is triggered, <strong>Actions</strong> are taken\",\n          params: {\n            enableScrolling: false,\n            placement: 'left',\n          }\n        },\n        {\n          target: \"#submit\",\n          content: \"Go ahead and save this route\",\n          params: {\n            enableScrolling: false,\n          },\n        },\n        {\n          target: \"#events\",\n          content: \"Let's check some incoming events\",\n          params: {\n            placement: \"right\",\n          }\n        },\n        {\n          target: \"#event-list\",\n          content: \"Expand to see details\"\n        },\n        {\n          target: \"\",\n          content: `You did it! Browse full docs at: <a href=\"https://aquasecurity.github.io/postee/latest/\" target=\"_blank\">Postee Docs</a>`\n        }\n      ]\n    };\n  },\n  computed: {\n    ...mapState({\n      error(state) {\n        return state.error.message;\n      },\n    }),\n    isOnLogin() {\n      return this.$route.name === 'login'\n    }\n  },\n  methods: {\n    doLogout() {\n      this.$store.dispatch(\"account/logout\");\n    },\n    startLoading() {\n      this.$store.dispatch(\"load\");\n      this.$store.dispatch(\"stats/load\");\n      this.$store.dispatch(\"events/load\");\n      this.startTour();\n    },\n    startTour(){\n        if (!localStorage.getItem('disable-tour')){\n          this.$tours['myTour'].start()\n      }else {\n          this.$tours['myTour'].finish()\n        }\n    },\n    stopTour(){\n      if (!localStorage.getItem('disable-tour')) {\n        localStorage.setItem('disable-tour', 'true');\n      }\n    }\n  },\n  mounted() {\n    if (this.$store.state.account.authenticated) {\n      this.startLoading();\n    } else {\n      if (!this.isOnLogin) {\n        this.$store\n          .dispatch(\"account/login\")\n          .then(() => {\n            this.startLoading();\n          })\n          .catch((error) => {\n            if (!error) {\n              //check failed - session is invalid\n              this.$router.push({ name: \"login\" });\n            }\n          });\n      }\n    }\n  },\n};\n</script>\n\n"
  },
  {
    "path": "ui/frontend/src/api.js",
    "content": "import axios from \"axios\";\nimport yaml from \"js-yaml\";\n\nconst transformYaml = (response) => {\n    try {\n        const json = yaml.load(response)\n        return json\n    } catch(e) {\n        return response //this way text errors are handled\n    }\n}\n\nexport default {\n    getConfig: function () {\n        return axios.get(\"/api/config\", { transformResponse: transformYaml })\n    },\n    getStats: function () {\n        return axios.get(\"/api/actions/stats\")\n    },\n    getEvents: function() {\n        return axios.get(\"/api/events\")\n    },\n    saveConfig: function (settings) {\n        const yamlObj = yaml.dump(settings)\n        return axios.post(\"/api/config\", yamlObj)\n\n    },\n    test: function (settings) {\n        return axios.post(\"/api/test\", settings)\n\n    },\n    login: function (username, password) {\n        const bodyFormData = new FormData();\n\n        bodyFormData.append('username', username ? username : \"\");\n        bodyFormData.append('password', password ? password : \"\");\n\n        return axios.post(\"/api/login\", bodyFormData)\n\n    },\n    logout: function () {\n        return axios.get(\"/api/logout\")\n    },\n    toApiPayload: function (context, modification) {\n        const rootState = context.rootGetters.getAppState\n        return {\n            actions: rootState.actions.all,\n            routes: rootState.routes.all,\n            templates: rootState.templates.all,\n            rules: rootState.rules.all,\n            ...rootState.settings.all,\n            ...modification\n        }\n    }\n}"
  },
  {
    "path": "ui/frontend/src/components/ActionCard.vue",
    "content": "<template>\n  <div class=\"col mb-4\">\n    <div class=\"card text-left\">\n      <div class=\"card-header\">\n        <b-icon-puzzle></b-icon-puzzle>\n        {{ type }}\n      </div>\n      <div class=\"card-body\">\n        <div class=\"d-flex align-items-center\">\n          <div class=\"flex-grow-1\">\n            <h5 class=\"card-title\">{{ isCommon ? \"Defaults\" : name }}</h5>\n          </div>\n        </div>\n\n        <h6 v-if=\"!isCommon\" class=\"card-subtitle text-muted\">\n          {{ scanCountMessage }}\n        </h6>\n      </div>\n      <div class=\"card-footer text-center\">\n        <router-link\n          :to=\"{ name: 'action', params: { name: name } }\"\n          class=\"btn btn-link\"\n          >Edit</router-link\n        >\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapState } from \"vuex\";\n\nexport default {\n  props: [\"type\", \"name\", \"enable\"],\n  data() {\n    return {};\n  },\n  computed: {\n    ...mapState({\n      scanCount(state) {\n        return state.stats.all[this.name];\n      },\n    }),\n    isCommon() {\n      return this.type === \"common\";\n    },\n    scanCountMessage() {\n      return this.scanCount === undefined\n        ? \"No scans received\"\n        : [\n            this.scanCount < 10000 ? this.scanCount : \"9999+\",\n            \" scan\",\n            this.scanCount === 1 ? \"\" : \"s\",\n            \" received\",\n          ].join(\"\");\n    },\n  },\n};\n</script>"
  },
  {
    "path": "ui/frontend/src/components/ActionDetails.vue",
    "content": "<template>\n  <div>\n    <div class=\"row justify-content-end pb-3 pr-3\">\n      <button type=\"submit\" class=\"btn btn-primary mr-2\" @click=\"doSubmit\">\n        Submit\n      </button>\n      <button\n        type=\"button\"\n        @click=\"doTest\"\n        class=\"btn btn-outline-primary mr-2\"\n      >\n        Test config\n      </button>\n      <b-spinner\n        v-if=\"isTestingInProgress\"\n        variant=\"primary\"\n        label=\"Spinning\"\n        class=\"mr-2\"\n      ></b-spinner>\n      <button\n        v-if=\"!!name\"\n        type=\"button\"\n        @click=\"doRemove\"\n        class=\"btn btn-outline-primary\"\n      >\n        Remove\n      </button>\n    </div>\n    <div class=\"card\">\n      <form @submit.prevent=\"doSubmit\">\n        <div class=\"card-body\">\n          <div class=\"row\">\n            <div class=\"col\">\n              <div class=\"form-group form-input\">\n                <label for=\"actionType\">Type</label>\n                <select\n                  class=\"form-select form-control\"\n                  :value=\"formValues.type\"\n                  id=\"actionType\"\n                  name=\"type\"\n                  @input=\"updateActionType\"\n                >\n                  <option value=\"email\">Email</option>\n                  <option value=\"jira\">Jira</option>\n                  <option value=\"slack\">Slack</option>\n                  <option value=\"teams\">Teams</option>\n                  <option value=\"webhook\">Webhook</option>\n                  <option value=\"splunk\">Splunk</option>\n                  <option value=\"serviceNow\">ServiceNow</option>\n                  <option value=\"nexusIq\">Nexus IQ</option>\n                  <option value=\"dependencytrack\">Dependency Track</option>\n                  <option value=\"exec\">Exec</option>\n                  <option value=\"awssecurityhub\">AWS Security Hub</option>\n                </select>\n                <small id=\"aHelp\" class=\"form-text text-muted\"\n                  >The action type</small\n                >\n              </div>\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"name\"\n                label=\"Name\"\n                :value=\"formValues.name\"\n                :errorMsg=\"errors['name']\"\n                :inputHandler=\"updateField\"\n                :validator=\"v(uniqueName)\"\n              />\n            </div>\n          </div>\n          <!-- enable is not required here-->\n          <CheckboxPropertyField\n            id=\"enable\"\n            label=\"Enable action\"\n            :value=\"formValues.enable\"\n            :inputHandler=\"updateField\"\n          />\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"user\"\n                label=\"User\"\n                :value=\"formValues.user\"\n                name=\"user\"\n                :show=\"showCredentials\"\n                :inputHandler=\"updateField\"\n                :errorMsg=\"errors['user']\"\n                :validator=\"v(required)\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"password\"\n                label=\"Password\"\n                inputType=\"password\"\n                :errorMsg=\"errors['password']\"\n                :value=\"formValues.password\"\n                name=\"password\"\n                :show=\"showCredentials\"\n                :inputHandler=\"updateField\"\n                :validator=\"v(required)\"\n              />\n            </div>\n          </div>\n          <PropertyField\n            id=\"url\"\n            label=\"Url\"\n            :value=\"formValues.url\"\n            :errorMsg=\"errors['url']\"\n            name=\"url\"\n            :description=\"getUrlDescription\"\n            :show=\"showUrl\"\n            :inputHandler=\"updateField\"\n            :validator=\"v([url, required])\"\n          />\n          <!-- nexus-iq custom properties start -->\n          <PropertyField\n            id=\"organizationId\"\n            label=\"Organization ID\"\n            :value=\"formValues['organization-id']\"\n            :errorMsg=\"errors['organizationId']\"\n            name=\"organization-id\"\n            description=\"ID of organization to create reports in\"\n            :show=\"isNexusIQ\"\n            :inputHandler=\"updateField\"\n            :validator=\"v(required)\"\n          />\n          <!-- -->\n          <!-- dependencytrack custom properties start -->\n          <PropertyField\n            id=\"dependencyTrackApiKey\"\n            label=\"Dependency Track API Key\"\n            :value=\"formValues['dependency-track-api-key']\"\n            :errorMsg=\"errors['dependencyTrackApiKey']\"\n            name=\"dependency-track-api-key\"\n            description=\"Key for API to upload BOM to Dependency Track\"\n            :show=\"isDependencyTrack\"\n            :inputHandler=\"updateField\"\n            :validator=\"v(required)\"\n          />\n          <!-- -->\n          <!-- exec custom properties start -->\n          <PropertyField\n              id=\"envVars\"\n              label=\"Environment variables\"\n              :value=\"formValues.envVars | toString \"\n              :errorMsg=\"errors['envVars']\"\n              name=\"env\"\n              description=\"Optional: comma separated list of environment variables to be set\"\n              :show=\"isExec\"\n              :inputHandler=\"updateCollectionField\"\n          />\n\n          <b-form-group v-if=\"isExec\" label=\"Select an input\" v-slot=\"{ ariaDescribedby }\">\n            <b-form-radio v-model=\"selectedExecInputParam\" :aria-describedby=\"ariaDescribedby\" name=\"exec-input\" value=\"file\">Input File</b-form-radio>\n            <b-form-radio v-model=\"selectedExecInputParam\" :aria-describedby=\"ariaDescribedby\" name=\"exec-input\" value=\"script\">Exec Script</b-form-radio>\n          </b-form-group>\n\n          <PropertyField\n              v-if='selectedExecInputParam===\"file\"'\n              id=\"input-file\"\n              label=\"Input File\"\n              :value=\"formValues['input-file']\"\n              :errorMsg=\"errors['input-file']\"\n              name=\"input-file\"\n              description=\"File path of custom shell script to execute\"\n              :show=\"isExec\"\n              :inputHandler=\"updateField\"\n              :validator=\"v(required)\"\n          />\n          <PropertyField\n              v-if='selectedExecInputParam===\"script\"'\n              id=\"exec-script\"\n              label=\"Exec Script\"\n              :value=\"formValues['exec-script']\"\n              :errorMsg=\"errors['exec-script']\"\n              name=\"exec-script\"\n              description=\"Content of shell script to execute\"\n              :show=\"isExec\"\n              :inputHandler=\"updateField\"\n              :validator=\"v(required)\"\n          />\n          <!-- exec custom properties end -->\n\n          <!-- email custom properties start -->\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"host\"\n                label=\"Host\"\n                :errorMsg=\"errors['host']\"\n                :value=\"formValues.host\"\n                description=\"Mandatory: SMTP host name (e.g. smtp.gmail.com)\"\n                :show=\"isEmail\"\n                :inputHandler=\"updateField\"\n                :validator=\"v(required)\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"port\"\n                label=\"Port\"\n                inputType=\"number\"\n                :errorMsg=\"errors['port']\"\n                :value=\"formValues.port\"\n                description=\"Mandatory: SMTP server port (e.g. 587)\"\n                :show=\"isEmail\"\n                :inputHandler=\"updateField\"\n                :validator=\"v(required)\"\n              />\n            </div>\n          </div>\n\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"sender\"\n                label=\"Sender\"\n                :value=\"formValues.sender\"\n                description=\"The email address to use as a sender\"\n                :errorMsg=\"errors['sender']\"\n                :show=\"isEmail\"\n                :inputHandler=\"updateField\"\n                :validator=\"v(email)\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"recipients\"\n                label=\"Recipients\"\n                :value=\"formValues.recipients | toString\"\n                description=\"Mandatory: comma separated list of recipients\"\n                :errorMsg=\"errors['recipients']\"\n                :show=\"isEmail\"\n                :inputHandler=\"updateCollectionField\"\n                :validator=\"v(recipients)\"\n              />\n            </div>\n          </div>\n\n          <CheckboxPropertyField\n            id=\"use-mx\"\n            label=\"Use MX\"\n            :value=\"formValues['use-mx']\"\n            :show=\"isEmail\"\n            :inputHandler=\"updateField\"\n          />\n          <!-- email custom properties end -->\n\n          <!-- jira custom properties start -->\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"jirauser\"\n                label=\"User\"\n                :value=\"formValues.user\"\n                name=\"user\"\n                :show=\"isJira\"\n                :inputHandler=\"updateField\"\n                :errorMsg=\"errors['user']\"\n                :validator=\"v(required)\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"jirapassword\"\n                label=\"Password\"\n                inputType=\"password\"\n                :errorMsg=\"errors['password']\"\n                :value=\"formValues.password\"\n                name=\"password\"\n                description=\"Optional. Specify Jira user password. API key can also be used for Cloud Jira instances\"\n                :show=\"isJira\"\n                :inputHandler=\"updateField\"\n                :validator=\"v(validateJiraPasswordandToken)\"\n              />\n            </div>\n          </div>\n          <PropertyField\n            id=\"jiratoken\"\n            label=\"Token\"\n            inputType=\"token\"\n            :errorMsg=\"errors['token']\"\n            :value=\"formValues.token\"\n            name=\"token\"\n            description=\"Optional. Specify Personal Access Token. Can be used for on-premise Jira only\"\n            :show=\"isJira\"\n            :inputHandler=\"updateField\"\n            :validator=\"v(validateJiraPasswordandToken)\"\n          />\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"projectKey\"\n                label=\"Project Key\"\n                name=\"project-key\"\n                :value=\"formValues['project-key']\"\n                :errorMsg=\"errors['project-key']\"\n                :description=\"'Mandatory. Specify the JIRA project key'\"\n                :show=\"isJira\"\n                :inputHandler=\"updateField\"\n                :validator=\"v(required)\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"board\"\n                label=\"Board\"\n                :value=\"formValues.board\"\n                description=\"Optional. Specify the Jira board name to open tickets on\"\n                :show=\"isJira\"\n                :inputHandler=\"updateField\"\n              />\n            </div>\n          </div>\n\n          <CheckboxPropertyField\n            id=\"tlsVerify\"\n            label=\"TLS verify\"\n            :value=\"formValues['tls-verify']\"\n            name=\"tls-verify\"\n            :show=\"isJira\"\n            :inputHandler=\"updateField\"\n          />\n\n\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"fixVersions\"\n                name=\"fix-versions\"\n                label=\"Fix Versions\"\n                :value=\"formValues['fix-versions'] | toString\"\n                description=\"Optional, specify comma separated list of Fix versions to add to Ticket\"\n                :show=\"isJira\"\n                :inputHandler=\"updateCollectionField\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"affectsVersions\"\n                name=\"affects-versions\"\n                label=\"Affects Versions\"\n                :value=\"formValues['affects-versions'] | toString\"\n                description=\"Optional, specify comma separated list of Affects versions to add to Ticket\"\n                :show=\"isJira\"\n                :inputHandler=\"updateCollectionField\"\n              />\n            </div>\n          </div>\n\n          <PropertyField\n            id=\"labels\"\n            label=\"Labels\"\n            :value=\"formValues.labels | toString\"\n            description=\"Optional, specify comma separated list of labels to add to Ticket\"\n            :show=\"isJira\"\n            :inputHandler=\"updateCollectionField\"\n          />\n\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"issuetype\"\n                label=\"Issue Type\"\n                :value=\"formValues.issuetype\"\n                description=\"Optional. Specify the issue type to open (Bug, Task, etc.). Default is Task\"\n                :show=\"isJira\"\n                :inputHandler=\"updateField\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"priority\"\n                label=\"Priority\"\n                :value=\"formValues.priority\"\n                description=\"Optional. Specify the issues severity. Default is High\"\n                :show=\"isJira\"\n                :inputHandler=\"updateField\"\n              />\n            </div>\n          </div>\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"assignee\"\n                label=\"Assignee\"\n                :value=\"formValues.assignee | toString\"\n                :description=\"jiraAssigneeDescription\"\n                :show=\"isJira\"\n                :inputHandler=\"updateCollectionField\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"sprint\"\n                label=\"Sprint\"\n                :value=\"formValues.sprint\"\n                description=\"Optional Sprint name, e.g., '3.5 Sprint 8'\"\n                :show=\"isJira\"\n                :inputHandler=\"updateField\"\n              />\n            </div>\n          </div>\n          <div class=\"row mb-3\" v-show=\"isJira\">\n            <div class=\"col-sm-7\">\n              <h5>Jira custom fields</h5>\n            </div>\n            <div class=\"col-sm-4 \">\n                <b-form-input\n                  id=\"cf-adder\"\n                  v-model=\"addedControl\"\n                  type=\"text\"\n                  placeholder=\"Add new custom field\"\n                ></b-form-input>\n            </div>\n            <div class=\"col-sm-1 text-right\">\n               <b-button variant=\"primary\" @click=\"addCf\" title=\"Add field\">Add</b-button>\n            </div>\n          </div>\n\n          <b-row class=\"form-group\" v-for=\"(cfValue, cfName) in unknowns\" v-bind:key=\"cfName\" :show=\"isJira\">\n            <b-col sm=\"2\">\n              <label :for=\"'cf-' + cfName\">{{cfName}}</label>\n            </b-col>\n            <b-col sm=\"9\">\n              <b-form-input :id=\"'cf-' + cfName\" :name=\"cfName\" v-model=\"unknowns[cfName]\"></b-form-input>\n            </b-col>\n            <b-col sm=\"1\" class=\"text-right\">\n              <b-button variant=\"primary\" title=\"Remove field\" @click=\"removeCf(cfName)\" ><span aria-hidden=\"true\">&times;</span></b-button>\n            </b-col>\n          </b-row>\n\n\n          <!-- jira custom properties end -->\n          <!-- serviceNow custom properties start -->\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"instance\"\n                label=\"Instance\"\n                :value=\"formValues.instance\"\n                description=\"Mandatory. Name of ServiceNow  or Instance\"\n                :errorMsg=\"errors['instance']\"\n                :show=\"isServiceNow\"\n                :inputHandler=\"updateField\"\n                :validator=\"v(required)\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"board\"\n                label=\"Board\"\n                :value=\"formValues.board\"\n                description=\"Specify the ServiceNow board name to open tickets on. Default is incident\"\n                :show=\"isServiceNow\"\n                :inputHandler=\"updateField\"\n              />\n            </div>\n          </div>\n          <!-- serviceNow custom properties end -->\n          <!-- splunk custom properties start -->\n          <div class=\"row\">\n            <div class=\"col\">\n              <PropertyField\n                id=\"token\"\n                label=\"Token\"\n                :value=\"formValues.token\"\n                :errorMsg=\"errors['token']\"\n                description=\"Mandatory. a HTTP Event Collector Token\"\n                :show=\"isSplunk\"\n                :inputHandler=\"updateField\"\n                :validator=\"v(required)\"\n              />\n            </div>\n            <div class=\"col\">\n              <PropertyField\n                id=\"sizeLimit\"\n                name=\"size-limit\"\n                label=\"Size Limit\"\n                :value=\"formValues['size-limit']\"\n                inputType=\"number\"\n                description=\"Optional. Maximum scan length, in bytes. Default: 10000\"\n                :show=\"isSplunk\"\n                :inputHandler=\"updateField\"\n              />\n            </div>\n          </div>\n          <!-- splunk custom properties end -->\n        </div>\n      </form>\n    </div>\n  </div>\n</template>\n<script>\n\nimport { mapState } from \"vuex\";\nimport ValidationMixin from \"./validator\";\nimport FormFieldMixin from \"./form\";\nimport PropertyField from \"./PropertyField.vue\";\nimport CheckboxPropertyField from \"./CheckboxPropertyField.vue\";\n\nconst urlDescriptionByType = {\n  splunk: \"Mandatory. Url of a Splunk server\",\n  webhook: \"Webhook's url\",\n  teams: \"Webhook's url\",\n  jira: 'Mandatory. E.g \"https://johndoe.atlassian.net\"',\n  slack: \"\",\n  nexusIq: \"Url of Nexus IQ server\",\n  dependencytrack: \"Url of Dependency Track server\",\n};\nconst typesWithCredentials = [\"serviceNow\", \"email\", \"nexusIq\"]; //TODO add description strings\n\nexport default {\n  data() {\n    return {\n      name: \"\",\n      addedControl: \"\",\n      unknowns: [],\n      isTestingInProgress: false,\n      fields: {}, //required for mixins\n      errors: {}, //required for mixins\n      actionType: \"\", //stored separately to track dependencies\n      jiraAssigneeDescription:\n        'Optional: comma separated list of users (emails) that will be assigned to ticket, e.g., [\"john@yahoo.com\"]. To assign a ticket to the Application Owner email address (as defined in Aqua Application Scope, owner email field), specify [\"<%application_scope_owner%>\"] as the assignee value',\n      selectedExecInputParam: 'script',\n    };\n  },\n  mixins: [FormFieldMixin, ValidationMixin],\n  components: {\n    PropertyField,\n    CheckboxPropertyField,\n  },\n  computed: {\n    ...mapState({\n      formValues(state) { //required for mixins\n        const found = state.actions.all.filter(\n          (item) => item.name === this.name\n        );\n\n        const result = found.length ? { ...found[0] } : { type: \"email\" };\n\n        this.actionType = result.type;\n        this.unknowns = {...result.unknowns}\n\n        return result;\n      },\n    }),\n    showUrl() {\n      return urlDescriptionByType[this.actionType] !== undefined;\n    },\n    getUrlDescription() {\n      return urlDescriptionByType[this.actionType];\n    },\n    isServiceNow() {\n      return this.actionType === \"serviceNow\";\n    },\n    isSplunk() {\n      return this.actionType === \"splunk\";\n    },\n    isEmail() {\n      return this.actionType === \"email\";\n    },\n    isJira() {\n      return this.actionType === \"jira\";\n    },\n    isNexusIQ() {\n      return this.actionType === \"nexusIq\";\n    },\n    isDependencyTrack() {\n      return this.actionType === \"dependencytrack\";\n    },\n    isExec(){\n      return this.actionType === \"exec\";\n    },\n    showCredentials() {\n      return typesWithCredentials.indexOf(this.actionType) >= 0;\n    },\n  },\n  filters: {\n    toString(col) {\n      return col ? col.join(\", \") : undefined;\n    },\n  },\n  methods: {\n    doTest() {\n      if (!this.isFormValid()) {\n        return;\n      }\n\n      this.isTestingInProgress = true;\n\n      this.$store\n        .dispatch(\"actions/test\", this.formValues)\n        .then(() => {\n          this.$bvToast.toast(\"Action is configured correctly\", {\n            title: \"Success\",\n            variant: \"success\",\n            autoHideDelay: 5000,\n          });\n          this.isTestingInProgress = false;\n        })\n        .catch((error) => {\n          this.$bvToast.toast(error, {\n            title: \"Connection error\",\n            variant: \"danger\",\n            autoHideDelay: 15000,\n          });\n          this.isTestingInProgress = false;\n        });\n    },\n    doSubmit() {\n      if (!this.isFormValid()) {\n        return;\n      }\n      //apply unknowns\n      if (Object.keys(this.unknowns).length > 0) {\n        this.formValues.unknowns={...this.unknowns}\n      }\n      if (this.name) {\n        this.$store.dispatch(\"actions/update\", {\n          value: this.formValues,\n          name: this.name,\n        });\n      } else {\n        this.$store.dispatch(\"actions/add\", this.formValues);\n      }\n      this.$router.push({ name: \"home\" });\n    },\n    updateActionType(e) {\n      this.actionType = e.target.value;\n      this.updateField(e);\n    },\n    doRemove() {\n      this.$store.dispatch(\"actions/remove\", this.name);\n      this.$router.push({ name: \"home\" });\n    },\n    uniqueName(label, value) {\n      if  (!value) {\n        return `${label} is required`\n      }\n      const found = this.$store.state.actions.all.filter(\n        (item) => item.name === value\n      );\n\n      if (found.length > 0 && found[0].name != this.name) {\n        return `${value} is not unique`\n      }\n      return false\n    },\n    addCf() {\n      const unknowns = {...this.unknowns}\n      unknowns[this.addedControl] = \"\"\n      this.addedControl = \"\"\n      this.unknowns = {...unknowns}\n    },\n    removeCf(propertyName) {\n      const unknowns = {...this.unknowns}\n      delete unknowns[propertyName]\n      this.unknowns = {...unknowns}\n    }\n\n  },\n  mounted() {\n    this.name = this.$route.params.name;\n  },\n};\n</script>"
  },
  {
    "path": "ui/frontend/src/components/Actions.vue",
    "content": "<template>\n<div >\n    <div class=\"row justify-content-end pb-3 pr-3\">\n        <router-link :to=\"{name: 'add-action'}\" class=\"btn btn-primary\">Add Action</router-link>\n    </div>\n  <div class=\"card-header\"><h3>Actions</h3></div>\n  <div class=\"card-body\">An Action is a enforceable step that enables the Postee operator to act upon incoming events.</div>\n    <div class=\"row row-cols-1 row-cols-md-3\">\n        <ActionCard v-for=\"(action, index) in actions\"\n            :key=\"index\"\n            :type=\"action.type\"\n            :name=\"action.name\"\n            :enable=\"action.enable\">\n        </ActionCard>\n    </div>\n</div>\n</template>\n<script>\nimport ActionCard from './ActionCard.vue';\nimport { mapState} from 'vuex'\n\nexport default {\n    components: {\n        ActionCard\n    },\n    computed: {\n        ...mapState({\n            actions (state) {\n                return state.actions.all\n            }\n        })\n    }\n\n}\n</script>"
  },
  {
    "path": "ui/frontend/src/components/CheckboxPropertyField.vue",
    "content": "<template>\n  <div class=\"form-group form-check\" v-if=\"show\">\n    <input\n      type=\"checkbox\"\n      class=\"form-check-input\"\n      :id=\"id\"\n      :checked=\"value\"\n      @input=\"inputHandler\"\n      :name=\"name || id\"\n    />\n    <label class=\"form-check-label\" :for=\"id\">{{ label }}</label>\n    <small v-show=\"!!description\" class=\"form-text text-muted\">{{\n      description\n    }}</small>\n  </div>\n</template>\n<script>\nexport default {\n  props: {\n    name: String,\n    label: String,\n    value: Boolean,\n    id: String,\n    description: String,\n    show: {\n      type: Boolean,\n      default: true,\n    },\n    inputHandler: Function,\n  },\n};\n</script>"
  },
  {
    "path": "ui/frontend/src/components/EventDetails.vue",
    "content": "<template>\n  <div id=\"event-list\">\n    <div class=\"card-header\"><h3>Events</h3></div>\n    <div class=\"card-body\">All incoming events that Postee has received so far.</div>\n    <json-viewer\n        :value=\"events\"\n        :expand-depth=3\n        copyable\n    ></json-viewer>\n  </div>\n</template>\n<script>\nimport { mapState } from 'vuex'\nimport JsonViewer from 'vue-json-viewer'\n\nexport default {\n  components:{\n    JsonViewer,\n  },\n\n  computed: {\n    ...mapState({\n      events (state) {\n        return state.events.all\n      }\n    })\n  },\n}\n\n</script>"
  },
  {
    "path": "ui/frontend/src/components/LoginForm.vue",
    "content": "<template>\n  <form class=\"form-signin text-center\" @submit.prevent=\"doSubmit\">\n    <img class=\"mb-2\" src=\"postee_new.svg\" alt=\"Welcome to Postee\" width=\"140\" height=\"170\">\n    <h1 class=\"h3 mb-3 font-weight-normal\">Login</h1>\n    <label for=\"username\" class=\"sr-only\">Username</label>\n    <input\n      type=\"username\"\n      id=\"username\"\n      class=\"form-control mb-3\"\n      v-model=\"username\"\n      placeholder=\"Username\"\n      required=\"\"\n      autofocus=\"\"\n    />\n    <label for=\"password\" class=\"sr-only\">Password</label>\n    <input\n      type=\"password\"\n      id=\"inputPassword\"\n      class=\"form-control mb-3\"\n      v-model=\"password\"\n      v-on:keyup.enter=\"doSubmit\"\n      placeholder=\"Password\"\n      required=\"\"\n    />\n    <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">\n      Sign in\n    </button>\n  </form>\n</template>\n<style>\n.form-signin {\n  width: 100%;\n  max-width: 330px;\n  padding: 15px;\n  margin: 0 auto;\n}\n</style>\n<script>\n\nexport default {\n  data() {\n    return {\n      username: \"\",\n      password: \"\",\n    };\n  },\n  methods: {\n    doSubmit() {\n      this.$store.dispatch(\"account/login\", {\n        username: this.username,\n        password: this.password,\n      }).then(()=>{\n        this.$router.push({ name: \"home\" })\n      }).catch(()=>{});\n    },\n  },\n};\n</script>"
  },
  {
    "path": "ui/frontend/src/components/PropertyField.vue",
    "content": "<template>\n  <div v-if=\"show\" class=\"form-group form-input\" v-bind:class=\"{ 'row pr-3' : inline }\">\n    <label v-bind:class=\"{ 'col-form-label col-sm-2': inline, 'form-label': !inline }\" :for=\"id\">{{ label }}</label>\n    <input\n      :type=\"inputType\"\n      :value=\"value\"\n      :name=\"nameOrId\"\n      @input=\"inputHandler\"\n      class=\"form-control\"\n      v-bind:class=\"{ 'is-invalid' : errorMsg , 'col-sm-10' : inline }\"\n      :id=\"id\"\n    />\n    <small v-show=\"!!description\" class=\"form-text text-muted\">{{\n      description\n    }}</small>\n    <div class=\"form-text invalid-feedback\">{{ errorMsg }}</div>\n  </div>\n</template>\n<script>\nexport default {\n  data() {\n    return {\n    }\n  },\n  computed: {\n    nameOrId(){\n      return this.name || this.id\n    },\n  },\n  props: {\n    errorMsg : [String, Boolean],\n    name: String,\n    inputType: {\n      type: String,\n      default: \"input\",\n    },\n    label: String,\n    value: [String, Number],\n    id: String,\n    description: String,\n    show: {\n      type: Boolean,\n      default: true,\n    },\n    inline:{\n      type: Boolean,\n      default: false,\n    },\n    inputHandler: Function,\n    validator: Object,\n  },\n  methods : {\n  },\n  mounted()  {\n    if (this.validator) {\n      this.validator.register(this.id, this.label, this.nameOrId)\n    }\n  }\n};\n</script>"
  },
  {
    "path": "ui/frontend/src/components/RouteCard.vue",
    "content": "<template>\n  <div class=\"col mb-4\">\n    <div class=\"card text-left\">\n      <div class=\"card-header\">\n        <b-icon-shuffle></b-icon-shuffle>\n      </div>\n      <div class=\"card-body\">\n        <div class=\"d-flex align-items-center\">\n          <div class=\"flex-grow-1\">\n            <h5 class=\"card-title\">{{ name }}</h5>\n          </div>\n        </div>\n\n        <h6 class=\"card-subtitle text-muted\">\n          {{ actionCnt }} actions configured\n        </h6>\n      </div>\n      <div class=\"card-footer text-center\">\n        <router-link\n          :to=\"{ name: 'route', params: { name: name } }\"\n          class=\"btn btn-link\"\n          >Edit</router-link\n        >\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\n\nexport default {\n  props: [\"name\", \"actionCnt\"],\n  data() {\n    return {};\n  }\n};\n</script>"
  },
  {
    "path": "ui/frontend/src/components/RouteDetails.vue",
    "content": "<template>\n  <div>\n    <div class=\"row justify-content-end pb-3 pr-3\">\n      <button id=\"submit\" type=\"submit\" class=\"btn btn-primary mr-2\" @click=\"doSubmit\">\n        Submit\n      </button>\n      <button\n        v-if=\"!!name\"\n        type=\"button\"\n        @click=\"doRemove\"\n        class=\"btn btn-outline-primary\"\n      >\n        Remove\n      </button>\n\n    </div>\n    <div class=\"card\">\n      <form @submit.prevent=\"doSubmit\">\n        <div class=\"card-body\">\n          <PropertyField\n            :id=\"'name'\"\n            :label=\"'Route Name'\"\n            :value=\"formValues.name\"\n            :errorMsg=\"errors['name']\"\n            :inputHandler=\"updateField\"\n            :validator=\"v(uniqueName)\"\n          />\n          <div id=\"select-input-policies\"> <h5>Input Policies</h5> </div>\n          <b-form-group>\n            <small class=\"form-text text-muted\">\n              Select rego policies to trigger the route\n            </small>\n            <div v-show=\"!custom_rule_enabled\">\n              <b-form-checkbox-group\n                  id=\"rules\"\n                  v-model=\"formValues['input-files']\"\n                  :options=\"availableRegoRules\"\n                  name=\"rules\"\n                  stacked\n              ></b-form-checkbox-group>\n            </div>\n\n            <br/>\n            <b-form-checkbox\n                  id=\"custom_rule\"\n                  v-model=\"custom_rule_enabled\"\n                  name=\"custom_rule\"\n                  value=true\n                  button button-variant=\"primary\"\n                  size=\"sm\"\n              >Custom Rego Rule\n            </b-form-checkbox>\n\n          </b-form-group>\n\n          <div v-if=\"custom_rule_enabled\" class=\"form-group form-input\">\n            <codemirror\n              :value=\"formValues.input\"\n              :options=\"cmOptions\"\n              id=\"input\"\n              name=\"input\"\n              @input=\"updateInput\"\n            >\n            </codemirror>\n            <small class=\"form-text text-muted\">\n              Set of REGO rules to filter received events. Leave empty to\n              process all incoming events\n            </small>\n          </div>\n\n          <h5>Actions</h5>\n          <b-form-group>\n            <small class=\"form-text text-muted\">\n              Select actions to perform when policy is triggered\n            </small>\n            <b-form-checkbox-group\n              id=\"actions\"\n              v-model=\"formValues.actions\"\n              :options=\"availableActions\"\n              name=\"actions\"\n            ></b-form-checkbox-group>\n          </b-form-group>\n\n          <br/>\n          <b-button v-b-toggle.plugins-collapse variant=\"primary\" size=\"sm\">Advanced Options</b-button>\n          <div>\n            <b-collapse id=\"plugins-collapse\" class=\"mt-2\">\n            <b-card>\n              Template\n              <div class=\"form-group form-input\">\n                <small id=\"aHelp\" class=\"form-text text-muted\"\n                >Select template to render events</small>\n                <select\n                    class=\"form-select form-control\"\n                    v-model=\"formValues.template\"\n                    id=\"template\"\n                    name=\"template\"\n                >\n                  <option\n                      v-for=\"template in availableTemplates\"\n                      v-bind:key=\"template\"\n                      :value=\"template\"\n                  >\n                    {{ template }}\n                  </option>\n                </select>\n              </div>\n            </b-card>\n\n              <b-card>\n                <p class=\"card-text\">\n                <PropertyField\n                    class=\"mb-4\"\n                    id=\"aggregateMessageNumber\"\n                    label=\"Aggregate-Message-Number\"\n                    :value=\"\n                formValues.plugins\n                  ? formValues.plugins['aggregate-message-number']\n                  : undefined\n              \"\n                    inputType=\"number\"\n                    name=\"aggregate-message-number\"\n                    description=\"Optional: Aggregate multiple messages into one ticket/message\tNumeric number. Default is 1\"\n                    :inputHandler=\"updateRoutePluginField\"\n                />\n                </p>\n              </b-card>\n              <b-card>\n                <p class=\"card-text\">\n                  <PropertyField\n                      class=\"mb-4\"\n                      id=\"aggregateMessageTimeout\"\n                      label=\"Aggregate-Message-Timeout\"\n                      :value=\"\n                formValues.plugins\n                  ? formValues.plugins['aggregate-message-timeout']\n                  : undefined\n              \"\n                      name=\"aggregate-message-timeout\"\n                      description=\"Optional: Aggregate multiple messages over period of time into one ticket/message\tXs (X number of seconds), Xm (X number of minutes), xH (X number of hours)\"\n                      :inputHandler=\"updateRoutePluginField\"\n                  />\n                </p>\n              </b-card>\n\n              <b-card>\n                <p class=\"card-text\">\n                  <PropertyField\n                      class=\"mb-4\"\n                      id=\"uniqueMessageProps\"\n                      label=\"Unique Message Props\"\n                      name=\"unique-message-props\"\n                      :value=\"\n                formValues.plugins && formValues.plugins['unique-message-props']\n                  ? formValues.plugins['unique-message-props'].join(', ')\n                  : undefined\n              \"\n                      description=\"Optional: Comma separated list of top level properties which unique identify an event message. If message with same id is received more than once it will be ignored\"\n                      :inputHandler=\"updateRoutePluginCollectionField\"\n                  />\n                </p>\n              </b-card>\n\n              <b-card>\n                <p class=\"card-text\">\n                  <PropertyField\n                      class=\"mb-4\"\n                      id=\"uniqueMessageTimeout\"\n                      label=\"Unique-Message-Timeout\"\n                      :value=\"\n                formValues.plugins\n                  ? formValues.plugins['unique-message-timeout']\n                  : undefined\n              \"\n                      name=\"unique-message-timeout\"\n                      description=\"Optional: Number of seconds/minutes/hours/days before expiring of a message. Expired messages are removed from db. If option is empty message is never deleted\"\n                      :inputHandler=\"updateRoutePluginField\"\n                  />\n                </p>\n              </b-card>\n\n            </b-collapse>\n          </div>\n        </div>\n      </form>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapState } from \"vuex\";\nimport ValidationMixin from \"./validator\";\nimport FormFieldMixin from \"./form\";\nimport PropertyField from \"./PropertyField.vue\";\n\nimport { codemirror } from \"vue-codemirror\";\n\nimport \"codemirror-rego/mode\";\nimport \"codemirror/lib/codemirror.css\";\n\nexport default {\n  data() {\n    return {\n      fields: {},\n      errors: {},\n      name: \"\",\n      cmOptions: {\n        tabSize: 4,\n        mode: \"rego\",\n        lineNumbers: true,\n        line: true,\n      },\n      custom_rule_enabled: false,\n    };\n  },\n  mixins: [FormFieldMixin, ValidationMixin],\n  components: {\n    PropertyField,\n    codemirror,\n  },\n  computed: {\n    ...mapState({\n      formValues(state) {\n        //required for mixins\n        const found = state.routes.all.filter(\n          (item) => item.name === this.name\n        );\n\n        const result = found.length ? { ...found[0] } : {};\n\n        if (!result.action) {\n          result.action = [];\n        }\n\n        return result;\n      },\n      availableActions(state) {\n        return state.actions.all.map((item) => item.name);\n      },\n      availableTemplates(state) {\n        return state.templates.all.map((item) => item.name);\n      },\n      availableRegoRules(state){\n        return state.rules.all.map((item) => item.name);\n      },\n    }),\n  },\n  methods: {\n    doSubmit() {\n      if (!this.isFormValid()) {\n        return;\n      }\n\n      if (this.name) {\n        this.$store.dispatch(\"routes/update\", {\n          value: this.formValues,\n          name: this.name,\n        });\n      } else {\n        this.$store.dispatch(\"routes/add\", this.formValues);\n      }\n      this.$router.push({ name: \"routes\" });\n    },\n    doRemove() {\n      this.$store.dispatch(\"routes/remove\", this.name);\n      this.$router.push({ name: \"routes\" });\n    },\n    uniqueName(label, value) {\n      if (!value) {\n        return `${label} is required`;\n      }\n      const found = this.$store.state.routes.all.filter(\n        (item) => item.name === value\n      );\n\n      if (found.length > 0 && found[0].name != this.name) {\n        return `${value} is not unique`;\n      }\n      return false;\n    },\n    updateInput(v) {\n      this.formValues.input = v;\n    },\n    updateRoutePluginField(e) {\n      if (!this.formValues.plugins) {\n        this.formValues.plugins = {};\n      }\n      const propName = e.target.attributes[\"name\"].value;\n      const inputType = e.target.attributes[\"type\"]?.value;\n      let v;\n      switch (inputType) {\n        case \"checkbox\": {\n          v = e.target.checked;\n          break;\n        }\n        case \"number\": {\n          v = Number(e.target.value);\n          break;\n        }\n        default: {\n          v = e.target.value;\n        }\n      }\n      this.formValues.plugins[propName] = v;\n    },\n    updateRoutePluginCollectionField(e) {\n      if (!this.formValues.plugins) {\n        this.formValues.plugins = {};\n      }\n      const propName = e.target.attributes[\"name\"].value;\n      const v = e.target.value.split(\",\").map((s) => s.trim());\n\n      this.formValues.plugins[propName] = v;\n    },\n    custom_rule(){\n      this.custom_rule_enabled = !this.custom_rule_enabled\n    }\n  },\n\n  mounted() {\n    this.name = this.$route.params.name;\n  },\n};\n</script>\n"
  },
  {
    "path": "ui/frontend/src/components/Routes.vue",
    "content": "<template>\n<div >\n    <div class=\"row justify-content-end pb-3 pr-3\">\n        <router-link id=\"add-route\" :to=\"{name: 'add-route'}\" class=\"btn btn-primary\">Add Route</router-link>\n    </div>\n  <div class=\"card-header\"><h3>Routes</h3></div>\n  <div class=\"card-body\">A route is used to control message flows. Each route includes the input message condition, the template that should be used to format the message, and the action(s) that the message should be delivered to.</div>\n    <div class=\"row row-cols-1 row-cols-md-3\">\n        <RouteCard v-for=\"(route, index) in routes\" :key=\"'route-' + index\" :name=\"route.name\" :actionCnt=\"actionCnt(route.actions)\"/>\n    </div>\n</div>\n</template>\n<script>\nimport RouteCard from './RouteCard.vue';\nimport { mapState} from 'vuex'\n\nexport default {\n    components: {\n        RouteCard\n    },\n    methods: {\n        actionCnt(actions) {\n            return actions?actions.length: 0\n        }\n    },\n    computed: {\n        ...mapState({\n            routes (state) {\n                return state.routes.all\n            }\n        })\n    }\n\n}\n</script>"
  },
  {
    "path": "ui/frontend/src/components/Settings.vue",
    "content": "<template>\n  <div>\n    <div class=\"row justify-content-end pb-3 pr-3\">\n      <button type=\"submit\" class=\"btn btn-primary mr-2\" @click=\"doSubmit\">\n        Submit\n      </button>\n    </div>\n    <div class=\"card-header\"><h3>Settings</h3></div>\n    <div class=\"card-body\">Modify current Postee settings.</div>\n    <div class=\"card\">\n      <form @submit.prevent=\"doSubmit\">\n        <div class=\"card-body\">\n          <PropertyField\n            :id=\"'tenant'\"\n            :label=\"'Name'\"\n            :value=\"formValues.name\"\n            :name=\"'name'\"\n            :description=\"'Tenant name'\"\n            :inputHandler=\"updateField\"\n          />\n          <PropertyField\n            :id=\"'aquaServer'\"\n            :label=\"'Aqua Server'\"\n            :value=\"formValues.AquaServer\"\n            :name=\"'AquaServer'\"\n            :description=\"'url of Aqua Server for links. E.g. https://myserver.aquasec.com'\"\n            :inputHandler=\"updateField\"\n          />\n          <PropertyField\n            :id=\"'maxDbSize'\"\n            :label=\"'Max Db size'\"\n            :inputType=\"'number'\"\n            :value=\"formValues.Max_DB_Size\"\n            :name=\"'Max_DB_Size'\"\n            :description=\"'Max size of DB. MB. if empty then unlimited'\"\n            :inputHandler=\"updateField\"\n          />\n          <PropertyField\n            :id=\"'deleteOldData'\"\n            :label=\"'Delete old data'\"\n            :inputType=\"'number'\"\n            :value=\"formValues.Delete_Old_Data\"\n            :name=\"'Delete_Old_Data'\"\n            :description=\"'delete data older than N day(s).  If empty then we do not delete.'\"\n            :inputHandler=\"updateField\"\n          />\n          <PropertyField\n            id=\"dbVerifyInterval\"\n            label=\"DB verify interval\"\n            inputType=\"number\"\n            :value=\"formValues.DbVerifyInterval\"\n            name=\"DbVerifyInterval\"\n            description=\"hours. an Interval between tests of DB. Default: 1 hour\"\n            :inputHandler=\"updateField\"\n          />\n        </div>\n      </form>\n    </div>\n  </div>\n</template>\n<script>\nimport { mapState } from \"vuex\";\nimport ValidationMixin from \"./validator\";\nimport FormFieldMixin from \"./form\";\nimport PropertyField from \"./PropertyField.vue\";\n\nexport default {\n  data() {\n    return {\n      fields: {},\n      errors: {},\n    };\n  },\n  mixins: [FormFieldMixin, ValidationMixin],\n  components: {\n    PropertyField,\n  },\n  computed: {\n    ...mapState({\n      formValues(state) {\n        return state.settings.all;\n      },\n    }),\n  },\n  methods: {\n    doSubmit() {\n        if (!this.isFormValid()) {\n            return;\n        }\n        this.$store.dispatch(\"settings/update\", this.formValues);\n    },\n  },\n};\n</script>\n"
  },
  {
    "path": "ui/frontend/src/components/TemplateCard.vue",
    "content": "<template>\n  <div class=\"col mb-4\">\n    <div class=\"card text-left\">\n      <div class=\"card-header\">\n        <b-icon-braces></b-icon-braces>\n      </div>\n      <div class=\"card-body\">\n        <div class=\"d-flex align-items-center\">\n          <div class=\"flex-grow-1\">\n            <h5 class=\"card-title\">{{ name }}</h5>\n          </div>\n        </div>\n\n      </div>\n      <div class=\"card-footer text-center\">\n        <router-link\n          :to=\"{ name: 'template', params: { name: name } }\"\n          class=\"btn btn-link\"\n          >Edit</router-link\n        >\n      </div>\n    </div>\n  </div>\n</template>\n\n<script>\n\nexport default {\n  props: [\"name\"],\n  data() {\n    return {};\n  }\n};\n</script>"
  },
  {
    "path": "ui/frontend/src/components/TemplateDetails.vue",
    "content": "<template>\n  <div>\n    <div class=\"row justify-content-end pb-3 pr-3\">\n      <button type=\"submit\" class=\"btn btn-primary mr-2\" @click=\"doSubmit\">\n        Submit\n      </button>\n      <button\n        v-if=\"!!name\"\n        type=\"button\"\n        @click=\"doRemove\"\n        class=\"btn btn-outline-primary\"\n      >\n        Remove\n      </button>\n    </div>\n    <div class=\"card\">\n      <form @submit.prevent=\"doSubmit\">\n        <div class=\"card-body\">\n          <PropertyField\n            :id=\"'name'\"\n            :label=\"'Name'\"\n            :value=\"formValues.name\"\n            :errorMsg=\"errors['name']\"\n            :inputHandler=\"updateField\"\n            :validator=\"v(uniqueName)\"\n          />\n          <div class=\"form-group form-input\">\n            <b-tabs content-class=\"mt-3\">\n              <b-tab title=\"Inline\" :active=\"!!formValues.body\">\n                <label class=\"form-label\" for=\"input\">REGO template:</label>\n                <codemirror\n                  :value=\"formValues.body\"\n                  :options=\"cmOptions\"\n                  id=\"body\"\n                  name=\"body\"\n                  @input=\"updateBody\"\n                >\n                </codemirror>\n                <small class=\"form-text text-muted\">\n                  REGO template to render received events\n                </small>\n              </b-tab>\n              <b-tab title=\"Package\" :active=\"!!formValues.regopackage\">\n                <PropertyField\n                  id=\"rego-package\"\n                  label=\"Package\"\n                  :value=\"formValues['rego-package']\"\n                  description=\"Rego package with template\"\n                  :inputHandler=\"updateTemplateSource\"\n                />\n              </b-tab>\n              <b-tab title=\"Url\" :active=\"!!formValues.url\">\n                <PropertyField\n                  id=\"url\"\n                  label=\"Url\"\n                  :value=\"formValues.url\"\n                  description=\"Url to load rego from\"\n                  :inputHandler=\"updateTemplateSource\"\n                  :validator=\"v(url, true)\"\n                  :errorMsg=\"errors['url']\"\n                />\n              </b-tab>\n              <b-tab title=\"Legacy\" :active=\"!!formValues.legacyScanRenderer\">\n                <div class=\"form-group form-input\">\n                  <label for=\"legacyScanRenderer\">Legacy</label>\n                  <select\n                    class=\"form-select form-control\"\n                    :value=\"formValues['legacy-scan-renderer']\"\n                    id=\"legacyScanRenderer\"\n                    name=\"legacy-scan-renderer\"\n                    @input=\"updateTemplateSource\"\n                  >\n                    <option value=\"html\">Html</option>\n                    <option value=\"slack\">Slack</option>\n                    <option value=\"jira\">Jira</option>\n                  </select>\n                  <small id=\"aHelp\" class=\"form-text text-muted\"\n                    >Use Postee v1 renderers</small\n                  >\n                </div>\n              </b-tab>\n            </b-tabs>\n\n\n          </div>\n        </div>\n      </form>\n    </div>\n  </div>\n</template>\n\n<script>\nimport { mapState } from \"vuex\";\nimport ValidationMixin from \"./validator\";\nimport FormFieldMixin from \"./form\";\nimport PropertyField from \"./PropertyField.vue\";\nimport { codemirror } from \"vue-codemirror\";\n\nimport \"codemirror-rego/mode\";\nimport \"codemirror/lib/codemirror.css\";\n\nexport default {\n  data() {\n    return {\n      fields: {},\n      errors: {},\n      name: \"\",\n      cmOptions: {\n        tabSize: 4,\n        mode: \"rego\",\n        lineNumbers: true,\n        line: true,\n      },\n    };\n  },\n  mixins: [FormFieldMixin, ValidationMixin],\n  components: {\n    PropertyField,\n    codemirror,\n  },\n  computed: {\n    ...mapState({\n      formValues(state) {\n        //required for mixins\n        const found = state.templates.all.filter(\n          (item) => item.name === this.name\n        );\n\n        const result = found.length ? { ...found[0] } : {};\n\n        return result;\n      },\n    }),\n  },\n  methods: {\n    doSubmit() {\n      if (!this.isFormValid()) {\n        return;\n      }\n\n      if (this.name) {\n        this.$store.dispatch(\"templates/update\", {\n          value: this.formValues,\n          name: this.name,\n        });\n      } else {\n        this.$store.dispatch(\"templates/add\", this.formValues);\n      }\n      this.$router.push({ name: \"templates\" });\n    },\n    doRemove() {\n      this.$store.dispatch(\"templates/remove\", this.name);\n      this.$router.push({ name: \"templates\" });\n    },\n    updateBody(v) {\n      this.formValues.body = v;\n      this.formValues.regopackage = undefined\n      this.formValues.url = undefined\n      this.formValues.legacyScanRenderer = undefined\n    },\n    updateTemplateSource(e) {\n      const srcProperties = [\"regopackage\", \"url\", \"legacyScanRenderer\"] //body is not cleared\n      const v = e.target.value;\n      const propName = e.target.attributes[\"name\"].value;\n\n      this.formValues[propName] = v;\n\n      srcProperties.filter(item=>item!=propName).forEach((item)=>{\n          this.formValues[item]=undefined\n      })\n    },\n    uniqueName(label, value) {\n      if (!value) {\n        return `${label} is required`;\n      }\n      const found = this.$store.state.templates.all.filter(\n        (item) => item.name === value\n      );\n\n      if (found.length > 0 && found[0].name != this.name) {\n        return `${value} is not unique`;\n      }\n      return false;\n    },\n  },\n  mounted() {\n    this.name = this.$route.params.name;\n  },\n};\n</script>\n"
  },
  {
    "path": "ui/frontend/src/components/Templates.vue",
    "content": "<template>\n<div >\n    <div class=\"row justify-content-end pb-3 pr-3\">\n        <router-link :to=\"{name: 'add-template'}\" class=\"btn btn-primary\">Add Template</router-link>\n    </div>\n  <div class=\"card-header\"><h3>Templates</h3></div>\n  <div class=\"card-body\">Templates are used to format input messages before sending them to the action.</div>\n    <div class=\"row row-cols-1 row-cols-md-3\">\n        <TemplateCard v-for=\"(template, index) in templates\" :key=\"'template-' + index\" :name=\"template.name\"/>\n    </div>\n</div>\n</template>\n<script>\nimport TemplateCard from './TemplateCard.vue';\nimport { mapState} from 'vuex'\n\nexport default {\n    components: {\n        TemplateCard\n    },\n    computed: {\n        ...mapState({\n            templates (state) {\n                return state.templates.all\n            }\n        })\n    }\n\n}\n</script>"
  },
  {
    "path": "ui/frontend/src/components/form.js",
    "content": "export default {\n    methods: {\n\n        updateField(e) {\n            const propName = e.target.attributes[\"name\"].value;\n            const inputType = e.target.attributes[\"type\"]?.value;\n            let v\n            switch(inputType) {\n                case \"checkbox\": {\n                    v = e.target.checked\n                    break;\n                }\n                case \"number\": {\n                    v = Number(e.target.value)\n                    break;\n                }\n                default: {\n                    v = e.target.value\n                }\n\n            }\n            this.formValues[propName] = v;\n        },\n        updateCollectionField(e) {\n            const propName = e.target.attributes[\"name\"].value;\n            const v = e.target.value.split(\",\").map((s) => s.trim());\n\n            this.formValues[propName] = v;\n        },\n        isFormValid() {\n            let firstElement;\n            this.errors = {}\n\n            for (const id in this.fields) {\n                const validator = this.fields[id];\n                const fieldValidations = Array.isArray(validator.validationFn)?validator.validationFn:[validator.validationFn];\n                const element = document.getElementById(id);\n\n                if (element) { //only elements in DOM are validated\n                    /*validator functions can be combined using AND*/\n                    fieldValidations.find(vfn=>{\n                        const r = vfn(\n                            validator.label,\n                            this.formValues[validator.name]\n                        );\n                        if (r) {\n                            this.errors[validator.name] = r;\n                            if (firstElement === undefined) {\n                                firstElement = element;\n                            }\n                            return true\n                        }\n                     })\n\n                }\n            }\n\n            firstElement && firstElement.focus();\n\n            return Object.keys(this.errors).length === 0;\n        }\n    }\n}"
  },
  {
    "path": "ui/frontend/src/components/validator.js",
    "content": "class validator {\n    constructor(fields, validationFn) {\n        this.validationFn = validationFn\n        this.register = (id, label, name) => {\n            this.label = label\n            this.name = name\n            fields[id] = this\n        }\n    }\n}\nexport default {\n    methods:\n    {\n        url(label, value) {\n            if (!value) {\n                return false\n            }\n            const errorMsg = `Invalid url : ${value}`\n            let url\n\n            try {\n                url = new URL(value);\n            } catch (_) {\n                return errorMsg;\n            }\n\n            return url.protocol === \"http:\" || url.protocol === \"https:\" ? false : errorMsg;\n        },\n\n        email(label, value) {\n            const re = /^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/\n            return re.test(String(value).toLowerCase()) ? false : `Invalid email '${value}'`\n        },\n\n        required(label, value) {\n            return !value ? `${label} is required` : false;\n        },\n\n        validateJiraPasswordandToken() {\n            if (!this.formValues.token && !this.formValues.password){\n                return `Password or token are required`\n            } \n            else if (this.formValues.token && this.formValues.password){\n                return `Password and token are filled`\n            }\n            return false;\n        },\n\n        recipients(label, value) {\n            const hasOneElement = value && value.length && value[0]\n            if (!hasOneElement) {\n                return `At least one of ${label} is required`\n            } else {\n                for (const email of value) {\n                    const v = this.email(\"-\", email);\n                    if (v) {\n                        return v;\n                    }\n                }\n            }\n            return false\n        },\n        v(validationFn) {\n            return new validator(this.fields, validationFn);\n        }\n\n\n    }\n}"
  },
  {
    "path": "ui/frontend/src/main.js",
    "content": "import Vue from 'vue'\nimport VueRouter from 'vue-router'\nimport App from './App.vue'\nimport Actions from './components/Actions.vue'\nimport LoginForm from './components/LoginForm.vue'\nimport ActionDetails from './components/ActionDetails.vue'\nimport RouteDetails from './components/RouteDetails.vue'\nimport Routes from './components/Routes.vue'\nimport TemplateDetails from './components/TemplateDetails.vue'\nimport Templates from './components/Templates.vue'\nimport Settings from './components/Settings.vue'\nimport Events from './components/EventDetails.vue'\nimport { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue'\nimport store from './store/store'\nimport 'bootstrap/dist/css/bootstrap.css'\nimport 'bootstrap-vue/dist/bootstrap-vue.css'\nimport VueTour from 'vue-tour'\n\nrequire('vue-tour/dist/vue-tour.css')\n\nVue.use(BootstrapVue);\nVue.use(BootstrapVueIcons);\nVue.use(VueRouter);\nVue.use(VueTour);\n\nconst routes = [\n  { name: 'home', path: '/', redirect: '/routes' },\n  { name: 'actions', path: '/actions', component: Actions },\n  { name: 'routes', path: '/routes', component: Routes },\n  { name: 'add-route', path: '/route', component: RouteDetails },\n  { name: 'route', path: '/route/:name', component: RouteDetails },\n  { name: 'settings', path: '/settings', component: Settings },\n  { name: 'login', path: '/login', component: LoginForm },\n  { name: 'add-action', path: '/action', component: ActionDetails },\n  { name: 'action', path: '/action/:name', component: ActionDetails },\n  { name: 'events', path: '/events', component: Events },\n\n  { name: 'templates', path: '/templates', component: Templates },\n  { name: 'add-template', path: '/template', component: TemplateDetails },\n  { name: 'template', path: '/template/:name', component: TemplateDetails }\n];\n\nexport const router = new VueRouter({\n  routes, mode: 'history'\n});\n\nnew Vue({\n  router,\n  store,\n  render: h => h(App),\n}).$mount('#app')\n"
  },
  {
    "path": "ui/frontend/src/store/modules/account.js",
    "content": "import api from \"../../api\"\nimport { router } from './../../main'\n\nexport default {\n    namespaced: true,\n    state: {\n        authenticated: false\n    },\n    actions: {\n        login(context, payload) {\n            const { username, password } = payload || {}\n            return new Promise((resolve, reject) => {\n                api.login(username, password).then(() => {\n                    context.commit(\"update\", { authenticated: true })\n                    context.commit(\"error/clear\", undefined, {root: true})\n                    resolve()\n                }).catch(error => {\n                    if (username && password) {\n                        const errorMsg = error.response.status === 401 ? \"Invalid credentials\" : error.response.data;\n                        context.commit(\"error/set\", errorMsg, {root: true})\n                        reject(errorMsg)\n                    } else {\n                        reject() //just checking\n                    }\n                })\n            })\n        },\n        logout(context) {\n            api.logout().then(() => {\n                context.commit(\"update\", { authenticated: false })\n                router.push({ name: \"login\" });\n            }).catch(error => {\n                context.commit(\"error/set\", error.response.data, {root: true})\n            })\n        },\n\n\n    },\n    mutations: {\n        update(state, info) {\n            state.userInfo = { ...info }\n        },\n\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/modules/actions.js",
    "content": "import api from \"../../api\"\nfunction updateActions(context, actions) {\n    api.saveConfig(api.toApiPayload(context, {actions})).then( //entire config is saved\n        context.commit(\"set\", actions)\n    ).catch((error) => {\n        context.commit(\"error/set\", error.response.data, {root:true})\n    })\n}\nexport default {\n    namespaced: true,\n    state: {all: []},\n    actions: {\n        test(context, settings) {\n            return new Promise((resolve, reject) => {\n                api.test(settings).then(() => {\n                    context.commit(\"error/clear\", undefined, {root:true})\n                    resolve();\n                }).catch(error => {\n                    if (error.response) {\n                        context.commit(\"error/set\", error.response.data, {root:true})\n                        reject(error.response.data);\n                    } else {\n                        console.error(error)\n                        reject(error);\n                    }\n                })\n            })\n        },\n\n        update(context, payload) {\n            const actions = context.state.all;\n            const { value, name } = payload\n\n            for (let i = 0; i < actions.length; i++) {\n                if (actions[i].name == name) {\n                    actions.splice(i, 1, value)\n                }\n            }\n            updateActions(context, actions)\n        },\n        remove(context, name) {\n            const actions = context.state.all.filter(item => item.name != name)\n\n            updateActions(context, actions)\n        },\n        add(context, settings) {\n            const actions = context.state.all\n            actions.push(settings)\n\n            updateActions(context, actions)\n        },\n\n\n    },\n    mutations: {\n        set(state, actions) {\n            state.all = [...actions]\n        },\n\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/modules/error.js",
    "content": "export default {\n    namespaced: true,\n    state: {\n        message: undefined\n    },\n    actions: {\n\n    },\n    mutations: {\n        set(state, error) {\n            state.message = error\n        },\n        clear(state) {\n            state.message = undefined\n        },\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/modules/events.js",
    "content": "import api from \"../../api\"\n\nexport default {\n    namespaced: true,\n    state: {\n        all: {}\n    },\n    actions: {\n        load(context) {\n            api.getEvents().then((response) => {\n                context.commit(\"set\", response.data)\n            }).catch((error) => {\n                context.commit(\"error/set\", error.response.data, {root: true})\n            })\n        },\n\n    },\n    mutations: {\n        set(state, payload) {\n            state.all = { ...payload }\n        },\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/modules/flags.js",
    "content": "\nexport default {\n    namespaced: true,\n    state: {all: {\n        loaded: false\n    }},\n    mutations: {\n        set(state, flags) {\n            state.all = {...state.all, ...flags}\n        }\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/modules/routes.js",
    "content": "import api from \"../../api\"\n\nfunction updateRoutes(context, routes) {\n    api.saveConfig(api.toApiPayload(context, {routes})).then(\n        context.commit(\"set\", routes)\n    ).catch((error) => {\n        context.commit(\"error/set\", error.response.data, {root:true})\n    })\n}\n\nexport default {\n    namespaced: true,\n    state: {all: []},\n    actions: {\n        update(context, payload) {\n            const routes = context.state.all;\n            const { value, name } = payload\n\n            for (let i = 0; i < routes.length; i++) {\n                if (routes[i].name == name) {\n                    routes.splice(i, 1, value)\n                }\n            }\n            updateRoutes(context, routes)\n        },\n        remove(context, name) {\n            const routes = context.state.all.filter(item => item.name != name)\n\n            updateRoutes(context, routes)\n        },\n        add(context, settings) {\n            const routes = context.state.all\n            routes.push(settings)\n\n            updateRoutes(context, routes)\n        },\n\n\n    },\n    mutations: {\n        set(state, routes) {\n            state.all = [...routes]\n        },\n\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/modules/rules.js",
    "content": "import api from \"../../api\"\n\nfunction updateRules(context, rules) {\n    api.saveConfig(api.toApiPayload(context, {rules})).then(\n        context.commit(\"set\", rules)\n    ).catch((error) => {\n        context.commit(\"error/set\", error.response.data, {root:true})\n    })\n}\n\nexport default {\n    namespaced: true,\n    state: {all: []},\n    actions: {\n        update(context, payload) {\n            const rules = context.state.all;\n            const { value, name } = payload\n\n            for (let i = 0; i < rules.length; i++) {\n                if (rules[i].name == name) {\n                    rules.splice(i, 1, value)\n                }\n            }\n            updateRules(context, rules)\n        },\n        remove(context, name) {\n            const rules = context.state.all.filter(item => item.name != name)\n\n            updateRules(context, rules)\n        },\n        add(context, settings) {\n            const rules = context.state.all\n            rules.push(settings)\n\n            updateRules(context, rules)\n        },\n\n\n    },\n    mutations: {\n        set(state, rules) {\n            state.all = [...rules]\n        },\n\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/modules/settings.js",
    "content": "import api from \"../../api\"\n\nexport default {\n    namespaced: true,\n    state: {all: {}},\n    actions: {\n        update(context, payload) {\n            api.saveConfig(api.toApiPayload(context, payload)).then( //entire config is saved\n                context.commit(\"set\", payload)\n            ).catch((error) => {\n                context.commit(\"error/set\", error.response.data, {root: true})\n            })\n\n        },\n    },\n    mutations: {\n        set(state, settings) {\n            state.all = {...settings}\n        },\n\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/modules/stats.js",
    "content": "import api from \"../../api\"\nexport default {\n    namespaced: true,\n    state: {\n        all: {}\n    },\n    actions: {\n        load(context) {\n            api.getStats().then((response) => {\n                context.commit(\"set\", response.data)\n            }).catch((error) => {\n                context.commit(\"error/set\", error.response.data, {root: true})\n            })\n        },\n\n    },\n    mutations: {\n        set(state, payload) {\n            state.all = { ...payload }\n        },\n\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/modules/templates.js",
    "content": "import api from \"../../api\"\n\nfunction updateTemplates(context, templates) {\n    api.saveConfig(api.toApiPayload(context, {templates})).then(\n        context.commit(\"set\", templates)\n    ).catch((error) => {\n        context.commit(\"error/set\", error.response.data, {root:true})\n    })\n}\n\nexport default {\n    namespaced: true,\n    state: {all: []},\n    actions: {\n        update(context, payload) {\n            const templates = context.state.all;\n            const { value, name } = payload\n\n            for (let i = 0; i < templates.length; i++) {\n                if (templates[i].name == name) {\n                    templates.splice(i, 1, value)\n                }\n            }\n            updateTemplates(context, templates)\n        },\n        remove(context, name) {\n            const templates = context.state.all.filter(item => item.name != name)\n\n            updateTemplates(context, templates)\n        },\n        add(context, settings) {\n            const templates = context.state.all\n            templates.push(settings)\n\n            updateTemplates(context, templates)\n        },\n\n\n    },\n    mutations: {\n        set(state, templates) {\n            state.all = [...templates]\n        },\n\n    }\n}"
  },
  {
    "path": "ui/frontend/src/store/store.js",
    "content": "import Vue from 'vue'\nimport Vuex from 'vuex'\nimport api from './../api'\n\nimport error from './modules/error.js'\nimport account from './modules/account.js'\nimport actions from './modules/actions.js'\nimport stats from './modules/stats.js'\nimport events from './modules/events.js'\nimport routes from './modules/routes.js'\nimport settings from './modules/settings.js'\nimport flags from './modules/flags.js'\nimport templates from './modules/templates.js'\nimport rules from './modules/rules.js'\n\nVue.use(Vuex)\n\nexport default new Vuex.Store({\n    modules: {\n        error,\n        actions,\n        account,\n        stats,\n        events,\n        routes,\n        settings,\n        flags,\n        templates,\n        rules\n    },\n    getters: {\n        getAppState(state) {\n            return state\n        }\n    },\n    actions: {\n        load(context) {\n            api.getConfig().then((response) => {\n                const data = response.data\n                // console.log(data.rules)\n                const settings = {\n                    name: data.name,\n                    AquaServer: data.AquaServer,\n                    Delete_Old_Data: data.Delete_Old_Data,\n                    DbVerifyInterval: data.DbVerifyInterval,\n                    Max_DB_Size: data.Max_DB_Size\n                }\n                data.actions && context.commit(\"actions/set\", data.actions)\n                data.routes && context.commit(\"routes/set\", data.routes)\n                data.templates && context.commit(\"templates/set\", data.templates)\n                data.events && context.commit(\"events/set\", data.events)\n                data.rules && context.commit(\"rules/set\", data.rules)\n                context.commit(\"settings/set\", settings)\n                context.commit(\"flags/set\", { loaded: true })\n            }).catch((error) => {\n                if (error.response) {\n                    context.commit(\"error/set\", error.response.data)\n                } else {\n                    console.error(error)\n                }\n            })\n        }\n    }\n\n})"
  },
  {
    "path": "ui/frontend/vue.config.js",
    "content": "// vue.config.js\n\n/**\n * @type {import('@vue/cli-service').ProjectOptions}\n */\nmodule.exports = {\n  devServer: {\n    proxy: 'http://localhost:8090'\n  }\n}"
  },
  {
    "path": "utils/cert.go",
    "content": "package utils\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"math/big\"\n\t\"net\"\n\t\"os\"\n\t\"time\"\n)\n\n// Generate a self-signed X.509 certificate.\n// Outputs will overwrite existing files.\n\nfunc publicKey(priv interface{}) interface{} {\n\tswitch k := priv.(type) {\n\tcase *rsa.PrivateKey:\n\t\treturn &k.PublicKey\n\tcase *ecdsa.PrivateKey:\n\t\treturn &k.PublicKey\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc pemBlockForKey(priv interface{}) *pem.Block {\n\tswitch k := priv.(type) {\n\tcase *rsa.PrivateKey:\n\t\treturn &pem.Block{Type: \"RSA PRIVATE KEY\", Bytes: x509.MarshalPKCS1PrivateKey(k)}\n\tcase *ecdsa.PrivateKey:\n\t\tb, err := x509.MarshalECPrivateKey(k)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t\treturn &pem.Block{Type: \"EC PRIVATE KEY\", Bytes: b}\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc generateCertificate(hosts []string, keyFile string, certFile string) error {\n\n\tvar priv interface{}\n\tvar err error\n\n\tpriv, err = rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnotBefore := time.Now()\n\tnotAfter := notBefore.Add(3650 * 24 * time.Hour)\n\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\tserialNumber, err := rand.Int(rand.Reader, serialNumberLimit)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: serialNumber,\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"Aquasec\"},\n\t\t},\n\t\tNotBefore:             notBefore,\n\t\tNotAfter:              notAfter,\n\t\tKeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t\tBasicConstraintsValid: true,\n\t}\n\n\tfor _, h := range hosts {\n\t\tif ip := net.ParseIP(h); ip != nil {\n\t\t\ttemplate.IPAddresses = append(template.IPAddresses, ip)\n\t\t} else {\n\t\t\ttemplate.DNSNames = append(template.DNSNames, h)\n\t\t}\n\t}\n\n\ttemplate.IsCA = true\n\ttemplate.KeyUsage |= x509.KeyUsageCertSign\n\n\tderBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcertOut, err := os.Create(certFile)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = pem.Encode(certOut, &pem.Block{Type: \"CERTIFICATE\", Bytes: derBytes})\n\tif err != nil {\n\t\treturn err\n\t}\n\tcertOut.Close()\n\n\tkeyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = pem.Encode(keyOut, pemBlockForKey(priv))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tkeyOut.Close()\n\treturn nil\n}\n\nfunc getHostnames() ([]string, error) {\n\n\tvar hosts []string\n\n\tifaces, err := net.Interfaces()\n\tif err != nil {\n\t\treturn hosts, err\n\t}\n\n\tfor _, iface := range ifaces {\n\n\t\tif iface.Flags&net.FlagUp == 0 {\n\t\t\tcontinue // interface down\n\t\t}\n\n\t\tif iface.Flags&net.FlagLoopback != 0 {\n\t\t\tcontinue // loopback interface\n\t\t}\n\n\t\taddrs, err := iface.Addrs()\n\t\tif err != nil {\n\t\t\treturn hosts, err\n\t\t}\n\n\t\tfor _, addr := range addrs {\n\t\t\tvar ip net.IP\n\t\t\tswitch v := addr.(type) {\n\t\t\tcase *net.IPNet:\n\t\t\t\tip = v.IP\n\t\t\tcase *net.IPAddr:\n\t\t\t\tip = v.IP\n\t\t\t}\n\n\t\t\tif ip == nil || ip.IsLoopback() {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tip = ip.To4()\n\t\t\tif ip == nil {\n\t\t\t\tcontinue // not an ipv4 address\n\t\t\t}\n\n\t\t\thosts = append(hosts, ip.String())\n\t\t\thh, _ := net.LookupAddr(ip.String())\n\t\t\tfor _, h := range hh {\n\t\t\t\thosts = append(hosts, h)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn hosts, nil\n}\n\nfunc GenerateCertificate(keyFile string, certFile string) error {\n\thosts, err := getHostnames()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn generateCertificate(hosts, keyFile, certFile)\n}\n"
  },
  {
    "path": "utils/prnheaders.go",
    "content": "package utils\n\nimport (\n\t\"io\"\n\t\"io/ioutil\"\n)\n\nfunc PrnLogResponse(body io.ReadCloser) string {\n\tdefer body.Close()\n\tmessage, _ := ioutil.ReadAll(body)\n\treturn string(message)\n}\n"
  },
  {
    "path": "utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"errors\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nvar (\n\tdbg = false\n)\n\nfunc GetEnvironmentVarOrPlain(value string) string {\n\tconst VarPrefix = \"$\"\n\tif strings.HasPrefix(value, VarPrefix) {\n\t\treturn os.Getenv(strings.TrimPrefix(value, VarPrefix))\n\t}\n\treturn value\n}\n\nfunc InitDebug() {\n\tif os.Getenv(\"AQUAALERT_DEBUG\") != \"\" {\n\t\tdbg = true\n\t}\n\tif os.Getenv(\"POSTEE_DEBUG\") != \"\" {\n\t\tdbg = true\n\t}\n}\n\nfunc Debug(format string, v ...interface{}) {\n\tif dbg != false {\n\t\tlog.Printf(format, v...)\n\t}\n}\n\nfunc GetEnv(name string) (string, error) {\n\tvalue := os.Getenv(name)\n\tif len(value) > 0 {\n\t\treturn value, nil\n\t}\n\treturn \"\", errors.New(\"not found\")\n}\n\n// GetRootDir returns the full path of the directory in which the process\n// is running.\nfunc GetRootDir() (string, error) {\n\treturn filepath.Abs(filepath.Dir(os.Args[0]))\n}\n\n// PathExists checks if a (full) path exists on the host/container.\nfunc PathExists(name string) bool {\n\t_, err := os.Stat(name)\n\treturn !os.IsNotExist(err)\n}\n"
  },
  {
    "path": "webserver/reload.go",
    "content": "package webserver\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/aquasecurity/postee/v2/router\"\n)\n\nfunc (web *WebServer) reload(w http.ResponseWriter, r *http.Request) {\n\trouter.Instance().ReloadConfig()\n}\n"
  },
  {
    "path": "webserver/tenant.go",
    "content": "package webserver\n\nimport (\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/aquasecurity/postee/v2/router\"\n\t\"github.com/aquasecurity/postee/v2/utils\"\n\t\"github.com/gorilla/mux\"\n)\n\nfunc (ctx *WebServer) tenantHandler(w http.ResponseWriter, r *http.Request) {\n\troute, ok := mux.Vars(r)[\"route\"]\n\tif !ok || len(route) == 0 {\n\t\tlog.Printf(\"Failed route: %q\", route)\n\t\tctx.writeResponse(w, http.StatusBadRequest, \"failed route\")\n\t\treturn\n\t}\n\n\tbody, err := ioutil.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Failed ioutil.ReadAll: %s\", err)\n\t\tctx.writeResponseError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdefer r.Body.Close()\n\tutils.Debug(\"%s\\n\\n\", string(body))\n\trouter.Instance().HandleRoute(route, body)\n\tctx.writeResponse(w, http.StatusOK, \"\")\n}\n"
  },
  {
    "path": "webserver/webserver.go",
    "content": "package webserver\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquasecurity/postee/v2/router\"\n\t\"github.com/aquasecurity/postee/v2/utils\"\n\t\"github.com/gorilla/mux\"\n)\n\ntype WebServer struct {\n\tquit   chan struct{}\n\trouter *mux.Router\n}\n\nvar initCtx sync.Once\nvar wsCtx *WebServer\n\nfunc Instance() *WebServer {\n\tinitCtx.Do(func() {\n\t\twsCtx = &WebServer{\n\t\t\tquit:   make(chan struct{}),\n\t\t\trouter: mux.NewRouter().StrictSlash(true),\n\t\t}\n\t})\n\treturn wsCtx\n}\nfunc (ctx *WebServer) withApiKey(next http.HandlerFunc) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tcorrectKey, err := dbservice.GetApiKey()\n\n\t\tif err != nil || correctKey == \"\" {\n\t\t\tlog.Printf(\"reload API key is either empty or there is an error: %s \\n\", err)\n\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t}\n\n\t\tif key := r.URL.Query().Get(\"key\"); key != correctKey {\n\t\t\tlog.Printf(\"reload API received an incorrect key %q\", key)\n\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t\treturn\n\t\t}\n\n\t\tnext(w, r)\n\t}\n}\n\nfunc (ctx *WebServer) Start(host, tlshost string) {\n\tlog.Printf(\"Starting WebServer....\")\n\n\trootDir, _ := utils.GetRootDir()\n\tcertPem := filepath.Join(rootDir, \"cert.pem\")\n\tkeyPem := filepath.Join(rootDir, \"key.pem\")\n\n\tif ok := utils.PathExists(keyPem); ok != true {\n\t\terr := utils.GenerateCertificate(keyPem, certPem)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"GenerateCertificate error: %v \\n\", err)\n\t\t}\n\t}\n\n\tif os.Getenv(\"AQUAALERT_CERT_PEM\") != \"\" {\n\t\tcertPem = os.Getenv(\"AQUAALERT_CERT_PEM\")\n\t}\n\n\tif os.Getenv(\"AQUAALERT_KEY_PEM\") != \"\" {\n\t\tkeyPem = os.Getenv(\"AQUAALERT_KEY_PEM\")\n\t}\n\terr := dbservice.EnsureApiKey()\n\tif err != nil {\n\t\tlog.Printf(\"EnsureApiKey error: %v \\n\", err)\n\t}\n\n\tctx.router.HandleFunc(\"/\", ctx.sessionHandler(ctx.scanHandler)).Methods(\"POST\")\n\tctx.router.HandleFunc(\"/tenant/{route}\", ctx.sessionHandler(ctx.tenantHandler)).Methods(\"POST\")\n\tctx.router.HandleFunc(\"/scan\", ctx.sessionHandler(ctx.scanHandler)).Methods(\"POST\")\n\tctx.router.HandleFunc(\"/ping\", ctx.sessionHandler(ctx.pingHandler)).Methods(\"GET\")\n\tctx.router.HandleFunc(\"/events\", ctx.sessionHandler(ctx.eventsHandler)).Methods(\"GET\")\n\n\tctx.router.HandleFunc(\"/reload\", ctx.withApiKey(ctx.reload)).Methods(\"GET\")\n\n\tgo func() {\n\t\tlog.Printf(\"Listening for HTTP on %s \", host)\n\t\tlog.Fatal(http.ListenAndServe(host, ctx.router))\n\t}()\n\tgo func() {\n\t\tlog.Printf(\"Listening for HTTPS on %s\", tlshost)\n\t\tlog.Fatal(http.ListenAndServeTLS(tlshost, certPem, keyPem, ctx.router))\n\t}()\n}\n\nfunc (ctx *WebServer) Terminate() {\n\tlog.Printf(\"Terminating WebServer....\")\n\tclose(ctx.quit)\n}\n\nfunc (ctx *WebServer) sessionHandler(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc {\n\treturn func(w http.ResponseWriter, r *http.Request) {\n\t\tf(w, r)\n\t}\n}\n\nfunc (ctx *WebServer) scanHandler(w http.ResponseWriter, r *http.Request) {\n\tbody, err := ioutil.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Failed ioutil.ReadAll: %s\\n\", err)\n\t\tctx.writeResponseError(w, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdefer r.Body.Close()\n\tutils.Debug(\"%s\\n\\n\", string(body))\n\trouter.Instance().Send(body)\n\tctx.writeResponse(w, http.StatusOK, \"\")\n}\n\nfunc (ctx *WebServer) pingHandler(w http.ResponseWriter, r *http.Request) {\n\tctx.writeResponse(w, http.StatusOK, \"Postee alive!\")\n}\n\nfunc (ctx *WebServer) writeResponse(w http.ResponseWriter, httpStatus int, v interface{}) {\n\tw.Header().Set(\"Content-Type\", \"application/json; charset=UTF-8\")\n\tw.WriteHeader(httpStatus)\n\tif v != nil {\n\t\tresult, _ := json.Marshal(v)\n\t\t_, err := w.Write(result)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Write error: %s \\n\", err)\n\t\t}\n\t}\n}\n\nfunc (ctx *WebServer) writeResponseError(w http.ResponseWriter, httpError int, err error) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\tw.WriteHeader(httpError)\n\terrEncode := json.NewEncoder(w).Encode(err)\n\tif errEncode != nil {\n\t\tlog.Printf(\"Encode error: %s \\n\", errEncode)\n\t}\n}\n\nfunc (ctx *WebServer) eventsHandler(w http.ResponseWriter, r *http.Request) {\n\tvar events []byte\n\tevents = append(events, []byte(\"[\")...)\n\tcurrentEvents := router.Instance().GetCurrentEvents()\n\tif len(currentEvents) > 0 && currentEvents != nil {\n\t\tfor i, ce := range currentEvents {\n\t\t\tif ce != nil {\n\t\t\t\tif i < len(currentEvents)-1 {\n\t\t\t\t\tevents = append(events, []byte(strings.Join([]string{string(ce.([]byte)), \",\"}, \"\"))...)\n\t\t\t\t} else {\n\t\t\t\t\tevents = append(events, ce.([]byte)...)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tevents = append(events, []byte(\"]\")...)\n\tw.WriteHeader(http.StatusOK)\n\t_, _ = w.Write(events)\n}\n"
  },
  {
    "path": "webserver/webserver_test.go",
    "content": "package webserver\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/router\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWebServer_eventsHandler(t *testing.T) {\n\trtr := router.Instance()\n\trtr.Send([]byte(`{\"SigMetadata\":{\"ID\":\"TRC-2\", \"hostname\":\"postee-0\"}}`))\n\trtr.Send([]byte(`{\"SigMetadata\":{\"ID\":\"TRC-3\", \"hostname\":\"postee-0\"}}`))\n\n\tws := WebServer{}\n\tw := httptest.NewRecorder()\n\tvar r *http.Request\n\tws.eventsHandler(w, r)\n\n\tresp := w.Result()\n\tdefer func() {\n\t\t_ = resp.Body.Close()\n\t}()\n\tgot, _ := ioutil.ReadAll(resp.Body)\n\n\tassert.JSONEq(t, `[\n   {\n      \"SigMetadata\":{\n         \"ID\":\"TRC-2\",\n         \"hostname\":\"postee-0\"\n      }\n   },\n   {\n      \"SigMetadata\":{\n         \"ID\":\"TRC-3\",\n         \"hostname\":\"postee-0\"\n      }\n   }\n]`, string(got))\n}\n"
  }
]