Repository: aquasecurity/postee Branch: main Commit: 4d31463ed7be Files: 335 Total size: 1.0 MB Directory structure: gitextract__yhalu2d/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── BUG_REPORT.md │ │ ├── FEATURE_REQUEST.md │ │ └── SUPPORT_QUESTION.md │ ├── dependabot.yml │ └── workflows/ │ ├── aqua-cloud.yml │ ├── go.yml │ ├── publish-chart.yml │ ├── publish-docs.yml │ └── release.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .yamllint ├── Dockerfile ├── Dockerfile.release ├── Dockerfile.ui ├── LICENSE ├── Makefile ├── README.md ├── actions/ │ ├── aws_securityhub.go │ ├── aws_securityhub_test.go │ ├── dependencytrack.go │ ├── dependencytrack_test.go │ ├── docker.go │ ├── docker_test.go │ ├── email.go │ ├── email_test.go │ ├── example/ │ │ └── exec/ │ │ └── defectdojo-curl-upload-scan.sh │ ├── exec.go │ ├── exec_test.go │ ├── goldens/ │ │ └── validbody.txt │ ├── http.go │ ├── http_test.go │ ├── jira.go │ ├── jira_test.go │ ├── kubernetes.go │ ├── kubernetes_test.go │ ├── message.go │ ├── message_test.go │ ├── nexusiq.go │ ├── nexusiq_test.go │ ├── opsgenie.go │ ├── opsgenie_test.go │ ├── pagerduty.go │ ├── pagerduty_test.go │ ├── plugin.go │ ├── servicenow.go │ ├── slack.go │ ├── splunk.go │ ├── stdout.go │ ├── teams.go │ ├── testdata/ │ │ └── nexus-iq-sbom.xml │ ├── webhook.go │ └── webhook_test.go ├── cfg.yaml ├── config/ │ ├── cfg-actions.yaml │ ├── cfg-controller-runner.yaml │ ├── cfg-docker-actions.yaml │ ├── cfg-k8s-actions.yaml │ ├── cfg-trivy-aws.yaml │ ├── cfg-trivy-operator-defectdojo.yaml │ ├── cfg-trivy-operator.yaml │ └── terminate-malicious-pods.yaml ├── controller/ │ └── controller.go ├── data/ │ ├── inpteval.go │ ├── slack.go │ ├── types.go │ ├── utils.go │ └── utils_test.go ├── dbservice/ │ ├── actions.go │ ├── changedbpath_test.go │ ├── checker.go │ ├── checker_test.go │ ├── dbaggregator.go │ ├── dbaggregator_test.go │ ├── dbparam.go │ ├── dbparam_test.go │ ├── dbservice_test.go │ ├── delete.go │ ├── init.go │ ├── insert.go │ ├── invalidinit_test.go │ ├── plgnstats.go │ ├── plgnstats_test.go │ ├── select.go │ ├── sharedcfg.go │ └── sharedcfg_test.go ├── deploy/ │ ├── helm/ │ │ └── postee/ │ │ ├── .helmignore │ │ ├── Chart.yaml │ │ ├── templates/ │ │ │ ├── NOTES.txt │ │ │ ├── _helpers.tpl │ │ │ ├── cfg-secret.yaml │ │ │ ├── ingress.yaml │ │ │ ├── postee-svc.yaml │ │ │ ├── postee-ui-secret.yaml │ │ │ ├── postee-ui-svc.yaml │ │ │ ├── postee-ui.yaml │ │ │ ├── postee.yaml │ │ │ ├── pvc.yaml │ │ │ ├── serviceaccount.yaml │ │ │ └── tests/ │ │ │ └── test-connection.yaml │ │ └── values.yaml │ └── kubernetes/ │ ├── hostPath/ │ │ └── postee-pv.yaml │ ├── postee-actions.yaml │ ├── postee-controller.yaml │ ├── postee-runner.yaml │ └── postee.yaml ├── docker-compose.yml ├── docs/ │ ├── actions/ │ │ └── actions.md │ ├── actions.md │ ├── advanced.md │ ├── aquacloud.md │ ├── blueprints/ │ │ ├── devops-pagerduty.md │ │ ├── external-healthcheck.md │ │ ├── image-processing.md │ │ ├── trivy-aws-security-hub.md │ │ ├── trivy-operator.md │ │ └── trivy-vulnerability-scan.md │ ├── config.md │ ├── controller-runner.md │ ├── demo.md │ ├── deployment.md │ ├── examples.md │ ├── improvements.md │ ├── index.md │ ├── install.md │ ├── routes.md │ ├── settings.md │ ├── templates.md │ ├── troubleshooting-of-rego-templates.md │ └── ui.md ├── formatting/ │ ├── eval.go │ ├── eval_test.go │ ├── htmlprovider.go │ ├── htmlprovider_test.go │ ├── jiraprovider.go │ ├── jiraprovider_test.go │ ├── markup_test.go │ ├── slackmrkdwnprovider.go │ └── slackmrkdwnprovider_test.go ├── go.mod ├── go.sum ├── integration/ │ ├── controller_runner_test.go │ └── goldens/ │ ├── client-cert.pem │ ├── client-key.pem │ ├── rootCA.pem │ ├── server-cert.pem │ ├── server-key.pem │ ├── simple.yaml │ └── test-seed.txt ├── layout/ │ ├── assurances.go │ ├── colors.go │ ├── malware.go │ ├── provider.go │ ├── sensitive.go │ ├── ticketLayout.go │ └── vulnerabilities.go ├── main.go ├── mkdocs.yml ├── msgservice/ │ ├── aggregatebytime_test.go │ ├── aggregatescan_test.go │ ├── applicationscopeowner_test.go │ ├── calculateexpired_test.go │ ├── getuniqueid_test.go │ ├── logs.go │ ├── msghandling.go │ ├── msgservice_mocks_test.go │ ├── msgservice_scan_test.go │ ├── msgservice_test.go │ ├── regocriteria_test.go │ ├── scheduler.go │ ├── scheduler_test.go │ ├── testdata/ │ │ ├── all-in-one-image.json │ │ └── collection-of-interfaces.json │ └── uniquemsgkey.go ├── overrides/ │ └── main.html ├── rego-filters/ │ ├── Allow-Image-Name.rego │ ├── Allow-Registry.rego │ ├── Credential Access │ ├── Defense Evasion │ ├── Ignore-Image-Name.rego │ ├── Ignore-Registry.rego │ ├── Initial Access │ ├── Persistence │ ├── Policy-Min-Vulnerability.rego │ ├── Policy-Only-Fix-Available.rego │ ├── Policy-Related-Features.rego │ ├── Privilege Escalation │ ├── Tracee Default Set │ └── Trivy AWS Findings ├── rego-templates/ │ ├── common/ │ │ └── common.rego │ ├── example/ │ │ ├── audit-html.rego │ │ └── defectdojo/ │ │ ├── trivy-operator-defectdojo.rego │ │ └── trivy-operator-defectdojo_test.rego │ ├── raw-message-html.rego │ ├── raw-message-json.rego │ ├── servicenow-incident.rego │ ├── servicenow-insight.rego │ ├── servicenow.rego │ ├── tracee-html.rego │ ├── tracee-slack.rego │ ├── trivy-jira.rego │ ├── trivy-operator-dependency-track.rego │ ├── trivy-operator-jira.rego │ ├── trivy-operator-slack.rego │ ├── trivy-vulns-slack.rego │ ├── trivy-vuls-slack-aggregation.rego │ ├── vuls-cyclonedx.rego │ ├── vuls-html-aggregation.rego │ ├── vuls-html.rego │ ├── vuls-opsgenie.rego │ ├── vuls-slack-aggregation.rego │ └── vuls-slack.rego ├── regoservice/ │ ├── aggregation_test.go │ ├── eval.go │ ├── eval_test.go │ ├── jsonformat.go │ ├── regocheck.go │ ├── regocheck_test.go │ └── testdata/ │ ├── goldens/ │ │ ├── html-with-complex-pkg.golden │ │ ├── html.golden │ │ ├── json-without-url.golden │ │ ├── json.golden │ │ ├── raw-message-html.golden │ │ ├── raw-message-json.golden │ │ ├── servicenow-incident.golden │ │ ├── servicenow-insight.golden │ │ ├── servicenow.golden │ │ ├── trivy-jira.golden │ │ ├── trivy-operator-jira.golden │ │ ├── trivy-operator-slack.golden │ │ ├── trivy-vulns-slack.golden │ │ ├── vuls-cyclonedx.golden │ │ ├── vuls-html.golden │ │ └── vuls-slack.golden │ ├── inputs/ │ │ ├── aqua-incident-input.json │ │ ├── aqua-input.json │ │ ├── aqua-insight-input.json │ │ ├── simple-input.json │ │ ├── trivy-input.json │ │ └── trivy-operator-input.json │ └── templates/ │ ├── common/ │ │ └── common.rego │ ├── html-with-complex-pkg.rego │ ├── html.rego │ ├── invalid.rego │ ├── json-without-url.rego │ ├── json.rego │ ├── without-any-expression.rego │ └── without-result.rego ├── router/ │ ├── anonymizeSettings_test.go │ ├── anonymizer.go │ ├── builders.go │ ├── goldens/ │ │ ├── kube-config.sample │ │ ├── sample.cfg │ │ └── test.txt │ ├── initoutputs_test.go │ ├── inittemplate_test.go │ ├── integrations.go │ ├── loads_test.go │ ├── parsecfg.go │ ├── parsecfg_test.go │ ├── routehandling_test.go │ ├── router.go │ ├── router_test.go │ ├── rule.go │ ├── sizeparser.go │ ├── sizeparser_test.go │ ├── template.go │ └── tenants.go ├── routes/ │ ├── aggrtimeout.go │ ├── aggrtimeout_test.go │ ├── routes.go │ └── routes_test.go ├── runner/ │ └── runner.go ├── servicenow/ │ ├── insert_table.go │ └── servicenow_base.go ├── slack/ │ └── sendtoslack.go ├── teams/ │ └── teams_requests.go ├── ui/ │ ├── backend/ │ │ ├── dbservice/ │ │ │ └── getplgnstats.go │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ └── uiserver/ │ │ ├── authentication.go │ │ ├── authentication_middleware.go │ │ ├── config.go │ │ ├── events.go │ │ ├── events_test.go │ │ ├── httpserver.go │ │ ├── plgnstats.go │ │ ├── server.go │ │ ├── testplg.go │ │ └── update_test.go │ └── frontend/ │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── package.json │ ├── public/ │ │ └── index.html │ ├── src/ │ │ ├── App.vue │ │ ├── api.js │ │ ├── components/ │ │ │ ├── ActionCard.vue │ │ │ ├── ActionDetails.vue │ │ │ ├── Actions.vue │ │ │ ├── CheckboxPropertyField.vue │ │ │ ├── EventDetails.vue │ │ │ ├── LoginForm.vue │ │ │ ├── PropertyField.vue │ │ │ ├── RouteCard.vue │ │ │ ├── RouteDetails.vue │ │ │ ├── Routes.vue │ │ │ ├── Settings.vue │ │ │ ├── TemplateCard.vue │ │ │ ├── TemplateDetails.vue │ │ │ ├── Templates.vue │ │ │ ├── form.js │ │ │ └── validator.js │ │ ├── main.js │ │ └── store/ │ │ ├── modules/ │ │ │ ├── account.js │ │ │ ├── actions.js │ │ │ ├── error.js │ │ │ ├── events.js │ │ │ ├── flags.js │ │ │ ├── routes.js │ │ │ ├── rules.js │ │ │ ├── settings.js │ │ │ ├── stats.js │ │ │ └── templates.js │ │ └── store.js │ └── vue.config.js ├── utils/ │ ├── cert.go │ ├── prnheaders.go │ └── utils.go └── webserver/ ├── reload.go ├── tenant.go ├── webserver.go └── webserver_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/BUG_REPORT.md ================================================ --- name: Bug Report labels: kind/bug about: If something isn't working as expected. --- ## Description ## What did you expect to happen? ## What happened instead? ## Output of run with `POSTEE_DEBUG=true`: ``` (paste your output here) ``` ## Additional details (environment setup, networking info...): ================================================ FILE: .github/ISSUE_TEMPLATE/FEATURE_REQUEST.md ================================================ --- name: Feature Request labels: kind/feature about: I have a suggestion (and might want to implement myself)! --- # Description # Use Case # Acceptance Criteria ================================================ FILE: .github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md ================================================ --- name: Support Question labels: triage/support about: If you have a question about Postee. --- ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: daily - package-ecosystem: docker directory: / schedule: interval: daily ================================================ FILE: .github/workflows/aqua-cloud.yml ================================================ name: Aqua Cloud on: push: branches: [ main ] schedule: - cron: '15 21 * * 2' jobs: build: name: Vulnerability Scan runs-on: "ubuntu-24.04" steps: - name: Checkout code uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner against Aqua Cloud uses: simar7/trivy-action@fe9b9e7e3c0d9e764d9c018d5603f57fba6aba3d # refer: https://github.com/actions/runner/issues/2033 with: scan-type: 'fs' hide-progress: true format: 'table' scanners: 'vuln,config' env: AQUA_KEY: ${{ secrets.AQUA_KEY }} AQUA_SECRET: ${{ secrets.AQUA_SECRET }} TRIVY_RUN_AS_PLUGIN: 'aqua' ================================================ FILE: .github/workflows/go.yml ================================================ --- name: Pull Request on: push: branches: [main] pull_request: branches: [main] env: GO_VERSION: "1.18" jobs: build: name: Checks runs-on: ubuntu-20.04 steps: - name: Set up Go uses: actions/setup-go@v4 with: go-version: ${{ env.GO_VERSION }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Setup golangci-lint uses: golangci/golangci-lint-action@v3.1.0 with: args: --timeout=10m --verbose version: v1.45 - name: Build run: make build - name: Run Unit Tests run: make test - name: Run Integration Tests run: make test-integration - name: Run Trivy vulnerability scanner in repo mode uses: aquasecurity/trivy-action@0.11.0 with: scan-type: 'fs' ignore-unfixed: true format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL' exit-code: 0 - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' - name: Run Trivy vulnerability scanner in IaC mode uses: aquasecurity/trivy-action@0.11.0 with: scan-type: 'config' hide-progress: false format: 'table' ================================================ FILE: .github/workflows/publish-chart.yml ================================================ # Triggered manually using as input the release e.g. v0.0.1 name: Publish Helm Chart on: pull_request: branches: - main paths: - 'deploy/helm/**' - 'deploy/kubernetes/**' push: tags: - "v*" workflow_dispatch: # manually it will get the latest tag to publish the helm chart env: HELM_REP: helm-charts GH_OWNER: aquasecurity CHART_DIR: deploy/helm/postee GO_VERSION: "1.18" KIND_VERSION: "v0.12.0" KIND_IMAGE: "kindest/node:v1.23.4@sha256:0e34f0d0fd448aa2f2819cfd74e99fe5793a6e4938b328f657c8e3f81ee0dfb9" jobs: publish-chart: if: github.event_name == 'push' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@dcd71f646680f2efd8db4afa5ad64fdcba30e748 with: fetch-depth: 0 - name: Install Helm uses: azure/setup-helm@v3.5 with: version: v3.6.0 - name: Install chart-releaser env: VERSION: 1.3.0 run: | wget "https://github.com/helm/chart-releaser/releases/download/v${VERSION}/chart-releaser_${VERSION}_linux_amd64.tar.gz" tar xzvf chart-releaser_${VERSION}_linux_amd64.tar.gz cr - name: 'Action Get Latest Tag' uses: 'actions-ecosystem/action-get-latest-tag@v1.6.0' id: 'get-latest-tag' with: semver_only: true - name: 'Determine default bump' id: 'bump' run: | LATEST_TAG=${{ steps.get-latest-tag.outputs.tag }} if [ "$LATEST_TAG" = "v0.0.0" ]; then echo "::set-output name=type::major" else echo "::set-output name=type::patch" fi - name: Package helm chart run: | RELEASE=${{ steps.get-latest-tag.outputs.tag }} echo "Release ${RELEASE}" helm package --app-version=${RELEASE} --version=${RELEASE} ${{ env.CHART_DIR }} -d .cr-release-packages - name: Upload helm chart # Failed with upload the same version: https://github.com/helm/chart-releaser/issues/101 continue-on-error: true ## Upload the tar in the Releases repository run: | ./cr upload -o ${{ env.GH_OWNER }} -r ${{ env.HELM_REP }} --token ${{ secrets.ORG_REPO_TOKEN }} -p .cr-release-packages - name: Index helm chart run: | ./cr index -o ${{ env.GH_OWNER }} -r ${{ env.HELM_REP }} -c https://${{ env.GH_OWNER }}.github.io/${{ env.HELM_REP }}/ -i index.yaml - name: Push index file uses: dmnemec/copy_file_to_another_repo_action@v1.0.4 env: API_TOKEN_GITHUB: ${{ secrets.ORG_REPO_TOKEN }} with: source_file: 'index.yaml' destination_repo: '${{ env.GH_OWNER }}/${{ env.HELM_REP }}' destination_folder: '.' destination_branch: 'gh-pages' user_email: aqua-bot@users.noreply.github.com user_name: 'aqua-bot' ================================================ FILE: .github/workflows/publish-docs.yml ================================================ --- # This is a manually triggered workflow to build and publish the MkDocs from the # specified Git revision to GitHub pages on https://aquasecurity.github.io/postee name: Publish Documentation on: workflow_dispatch: inputs: ref: description: The branch, tag or SHA to deploy, e.g. v0.0.1 required: true # Disable permissions granted to the GITHUB_TOKEN for all the available scopes. permissions: {} jobs: deploy: name: Deploy documentation runs-on: ubuntu-20.04 permissions: contents: write steps: - name: Checkout main uses: actions/checkout@v3 with: ref: ${{ github.event.inputs.ref }} fetch-depth: 0 persist-credentials: true - uses: actions/setup-python@v4 with: python-version: 3.x - run: | pip install git+https://${GH_TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git pip install mike pip install mkdocs-macros-plugin env: # Note: It is not the same as ${{ secrets.GITHUB_TOKEN }} ! GH_TOKEN: ${{ secrets.MKDOCS_AQUA_BOT }} - run: | git config user.name "aqua-bot" git config user.email "aqua-bot@users.noreply.github.com" - run: | mike deploy --push --update-aliases ${{ github.event.inputs.ref }} latest ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - "*" workflow_dispatch: env: GO_VERSION: "1.18" jobs: tests: name: Run Tests runs-on: ubuntu-20.04 steps: - name: Setup Go uses: actions/setup-go@v4 with: go-version: ${{ env.GO_VERSION }} - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 - name: Build run: make build - name: Run Unit tests run: | make test - name: Run Integration Tests run: make test-integration - name: Run Trivy vulnerability scanner in repo mode uses: aquasecurity/trivy-action@0.11.0 with: scan-type: 'fs' ignore-unfixed: true format: 'sarif' severity: 'CRITICAL' exit-code: 0 - name: Run Trivy vulnerability scanner against Aqua Cloud uses: aquasecurity/trivy-action@0.11.0 with: scan-type: 'fs' hide-progress: true format: 'table' security-checks: 'vuln,config' env: AQUA_KEY: ${{ secrets.AQUA_KEY }} AQUA_SECRET: ${{ secrets.AQUA_SECRET }} TRIVY_RUN_AS_PLUGIN: 'aqua' release: name: Release needs: - tests runs-on: ubuntu-20.04 steps: - name: Setup Go uses: actions/setup-go@v4 with: go-version: ${{ env.GO_VERSION }} - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USER }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to ECR uses: docker/login-action@v2 with: registry: public.ecr.aws username: ${{ secrets.ECR_ACCESS_KEY_ID }} password: ${{ secrets.ECR_SECRET_ACCESS_KEY }} - name: Release uses: goreleaser/goreleaser-action@v4 with: version: ~> 0.180 args: release --rm-dist workdir: . env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Log out from docker.io and ECR registries if: ${{ always() }} run: | docker logout docker.io docker logout public.ecr.aws ================================================ FILE: .gitignore ================================================ .idea/ bin/ pkg/ src/github.com/ src/gopkg.in/ src/go.etcd.io/ **/*.out **/*.db coverage.txt dist/ .vscode/ ================================================ FILE: .golangci.yml ================================================ run: timeout: 5m linters: enable: - errorlint - govet disable: - gosimple - ineffassign - staticcheck ================================================ FILE: .goreleaser.yml ================================================ project_name: postee release: draft: false prerelease: auto env: - GO111MODULE=on - CGO_ENABLED=0 before: hooks: - make build builds: - id: postee dir: . main: ./main.go binary: postee ldflags: - -s -w - "-extldflags '-static'" - -X main.version={{.Version}} goos: - darwin - linux goarch: - amd64 - arm - arm64 goarm: - 7 ignore: - goos: darwin goarch: 386 archives: - name_template: "{{ .ProjectName }}_{{.Version}}_{{ .Os }}_{{ .Arch }}" builds: - postee replacements: amd64: 64bit arm: ARM arm64: ARM64 darwin: macOS linux: Linux checksum: name_template: "checksums.txt" snapshot: name_template: "{{ .FullCommit }}" changelog: sort: asc filters: exclude: - '^docs' - '^test' - '^release' dockers: - dockerfile: Dockerfile.release use: buildx goos: linux goarch: amd64 image_templates: - "docker.io/aquasec/postee:{{ .Version }}-amd64" - "public.ecr.aws/aquasecurity/postee:{{ .Version }}-amd64" - "docker.io/aquasec/postee:latest" - "public.ecr.aws/aquasecurity/postee:latest" ids: - postee extra_files: - rego-templates/ - rego-filters/ - cfg.yaml build_flag_templates: - "--label=org.opencontainers.image.title={{ .ProjectName }}" - "--label=org.opencontainers.image.description=Command line interface for Postee" - "--label=org.opencontainers.image.vendor=Aqua Security" - "--label=org.opencontainers.image.version={{ .Version }}" - "--label=org.opencontainers.image.created={{ .Date }}" - "--label=org.opencontainers.image.source=https://github.com/aquasecurity/postee" - "--label=org.opencontainers.image.revision={{ .FullCommit }}" - "--platform=linux/amd64" - dockerfile: Dockerfile.ui use: buildx goos: linux goarch: amd64 image_templates: - "docker.io/aquasec/postee-ui:{{ .Version }}-amd64" - "public.ecr.aws/aquasecurity/postee-ui:{{ .Version }}-amd64" - "docker.io/aquasec/postee-ui:latest" - "public.ecr.aws/aquasecurity/postee-ui:latest" ids: - postee-ui extra_files: - rego-templates/ - rego-filters/ - cfg.yaml - ui/ build_flag_templates: - "--label=org.opencontainers.image.title={{ .ProjectName }}" - "--label=org.opencontainers.image.description=Postee UI" - "--label=org.opencontainers.image.vendor=Aqua Security" - "--label=org.opencontainers.image.version={{ .Version }}" - "--label=org.opencontainers.image.created={{ .Date }}" - "--label=org.opencontainers.image.source=https://github.com/aquasecurity/postee" - "--label=org.opencontainers.image.revision={{ .FullCommit }}" - "--label=org.opencontainers.image.documentation=https://aquasecurity.github.io/postee/v{{ .Version }}/" - "--platform=linux/amd64" docker_manifests: - name_template: 'aquasec/postee:{{ .Version }}' image_templates: - 'aquasec/postee:{{ .Version }}-amd64' - name_template: 'public.ecr.aws/aquasecurity/postee:{{ .Version }}' image_templates: - 'public.ecr.aws/aquasecurity/postee:{{ .Version }}-amd64' - name_template: 'aquasec/postee:latest' image_templates: - 'aquasec/postee:{{ .Version }}-amd64' # Postee-UI - name_template: 'aquasec/postee-ui:{{ .Version }}' image_templates: - 'aquasec/postee-ui:{{ .Version }}-amd64' - name_template: 'public.ecr.aws/aquasecurity/postee-ui:{{ .Version }}' image_templates: - 'public.ecr.aws/aquasecurity/postee-ui:{{ .Version }}-amd64' - name_template: 'aquasec/postee-ui:latest' image_templates: - 'aquasec/postee-ui:{{ .Version }}-amd64' ================================================ FILE: .yamllint ================================================ --- extends: default rules: line-length: disable truthy: disable document-start: disable ignore: | /src/ ================================================ FILE: Dockerfile ================================================ FROM golang:1.18-alpine as builder # RUN apk add --update git COPY . /server/ WORKDIR /server/ ARG TARGETOS TARGETARCH RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build --ldflags "-s -w" -o ./bin/postee main.go FROM alpine:3.18.2 RUN apk update && apk add wget ca-certificates curl jq EXPOSE 8082 EXPOSE 8445 RUN mkdir /server RUN mkdir /server/database RUN mkdir /config COPY --from=builder /server/bin /server/ COPY --from=builder /server/rego-templates /server/rego-templates COPY --from=builder /server/rego-filters /server/rego-filters COPY --from=builder /server/cfg.yaml /server/cfg.yaml WORKDIR /server RUN chmod +x postee RUN addgroup -g 1099 postee RUN adduser -D -g '' -G postee -u 1099 postee RUN chown -R postee:postee /server RUN chown -R postee:postee /config USER postee ENTRYPOINT ["/server/postee"] ================================================ FILE: Dockerfile.release ================================================ FROM alpine:3.18.2 RUN apk add --no-cache \ ca-certificates \ curl \ jq \ wget EXPOSE 8082 EXPOSE 8445 RUN mkdir /server RUN mkdir /server/database RUN mkdir /config COPY postee /server/ COPY rego-templates /server/rego-templates COPY rego-filters /server/rego-filters COPY cfg.yaml /config/ WORKDIR /server RUN chmod +x postee RUN addgroup -g 1099 postee RUN adduser -D -g '' -G postee -u 1099 postee RUN chown -R postee:postee /server RUN chown -R postee:postee /config USER postee ENTRYPOINT ["/server/postee"] ================================================ FILE: Dockerfile.ui ================================================ FROM node:18-alpine3.17 as vuebuilder COPY ./ui/frontend /frontend WORKDIR /frontend RUN yarn install RUN yarn build FROM golang:1.18-alpine as gobuilder COPY . /server WORKDIR /server/ui/backend RUN apk add git ARG TARGETOS TARGETARCH RUN GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build --ldflags "-s -w" -o posteeui FROM alpine:3.18.2 EXPOSE 8001 RUN mkdir /uiserver RUN mkdir /uiserver/www RUN mkdir /server RUN mkdir /server/database RUN mkdir /config COPY --from=gobuilder /server/ui/backend/posteeui /uiserver COPY --from=vuebuilder /frontend/dist /uiserver/www WORKDIR /uiserver RUN addgroup -g 1099 postee RUN adduser -D -g '' -G postee -u 1099 postee RUN chown -R postee:postee /server RUN chown -R postee:postee /config RUN chown -R postee:postee /uiserver USER postee ENTRYPOINT ["/uiserver/posteeui"] ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2016 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ # Set the default goal .DEFAULT_GOAL := build VERSION := $(shell git describe --tags) LDFLAGS=-ldflags "-s -w -X=main.version=$(VERSION)" # Active module mode, as we use Go modules to manage dependencies export GO111MODULE=on GO_FMT=gofmt .PHONY: build fmt vet test default : build .PHONY: build build : @echo "Building Postee...." CGO_ENABLED=0 go build $(LDFLAGS) -o ./postee main.go @echo "Done!" fmt : @echo "fmt...." $(GO_FMT) -s -w ./ test : go test -race -v -timeout=30s ./... test-integration: go test -race -v -tags=integration -timeout=30s ./... cover : go test ./msgservice ./dbservice ./router ./formatting ./data ./regoservice ./routes ./actions -v -coverprofile=cover.out go tool cover -html=cover.out composer : @echo "Running Postee UI...." docker-compose up --build docker-webhook : build @echo "Building image Dockerfile.release...." docker build --no-cache -t aquasec/postee:latest -f Dockerfile.release . docker run -p 8082:8082 -p 8445:8445 aquasec/postee:latest --cfgfile /server/cfg.yaml docker-ui : @echo "Building image Dockerfile.ui...." docker build --no-cache -t aquasec/postee-ui:latest -f Dockerfile.ui . deploy-k8s : @echo "Deploy Postee in Kubernetes...." kubectl create -f deploy/kubernetes kubectl wait --for=condition=available \ --timeout=1m deploy/postee ================================================ FILE: README.md ================================================ # Notice: Postee is no longer under active development or maintenance.

![Docker Pulls][docker-pull] [![Go Report Card][report-card-img]][report-card] ![](https://github.com/aquasecurity/postee/workflows/Go/badge.svg) [![License][license-img]][license] [download]: https://img.shields.io/github/downloads/aquasecurity/postee/total?logo=github [release-img]: https://img.shields.io/github/release/aquasecurity/postee.png?logo=github [release]: https://github.com/aquasecurity/postee/releases [docker-pull]: https://img.shields.io/docker/pulls/aquasec/postee?logo=docker&label=docker%20pulls%20%2F%20postee [go-doc-img]: https://godoc.org/github.com/aquasecurity/postee?status.svg [report-card-img]: https://goreportcard.com/badge/github.com/aquasecurity/postee [report-card]: https://goreportcard.com/report/github.com/aquasecurity/postee [license-img]: https://img.shields.io/badge/License-mit-blue.svg [license]: https://github.com/aquasecurity/postee/blob/master/LICENSE Postee is a simple message routing application that receives input messages through a webhook interface, and can take enforce actions using predefined outputs via integrations. Watch a quick demo of how you can use Postee: [![Postee Demo Video](./docs/img/postee-video-thumbnail.jpg)](https://www.youtube.com/watch?v=HZ5Z8jAVH8w) Primary 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. In addition, Postee can also be used to enforce pre-defined behaviours that can orchestrate actions based on input messages as triggers. ![Postee v2 scheme](docs/img/postee-v2-scheme.png) ## Status Although we are trying to keep new releases backward compatible with previous versions, this project is still incubating, and some APIs and code structures may change. ## Documentation The official [Documentation] provides detailed installation, configuration, troubleshooting, and quick start guides. --- Postee is an [Aqua Security](https://aquasec.com) open source project. Learn about our [Open Source Work and Portfolio]. Join the community, and talk to us about any matter in [GitHub Discussions] or [Slack]. [Documentation]: https://aquasecurity.github.io/postee/latest [Open Source Work and Portfolio]: https://www.aquasec.com/products/open-source-projects/ [Slack]: https://slack.aquasec.com/ [GitHub Discussions]: https://github.com/aquasecurity/postee/discussions ## Release 1. Bump version of [helm chart](https://github.com/aquasecurity/postee/blob/main/deploy/helm/postee/Chart.yaml). 1. (By repository admin) Create a new tag. Postee and helm charts are automatically released by github actions. 1. (By repository admin) Run [publish-docs workflow](https://github.com/aquasecurity/postee/blob/main/.github/workflows/publish-docs.yml), if document has been updated. ================================================ FILE: actions/aws_securityhub.go ================================================ package actions import ( "context" "encoding/json" "fmt" "log" "strings" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/securityhub" "github.com/aws/aws-sdk-go-v2/service/securityhub/types" ) type securityHubAPI interface { BatchImportFindings(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) } type Finding struct { SchemaVersion string `json:"SchemaVersion,omitempty"` ID string `json:"Id,omitempty"` ProductArn string `json:"ProductArn,omitempty"` GeneratorID string `json:"GeneratorId,omitempty"` AwsAccountID string `json:"AwsAccountId,omitempty"` Types []string `json:"Types,omitempty"` CreatedAt string `json:"CreatedAt,omitempty"` UpdatedAt string `json:"UpdatedAt,omitempty"` Severity struct { Label string `json:"Label,omitempty"` } `json:"Severity,omitempty"` Title string `json:"Title,omitempty"` Description string `json:"Description,omitempty"` Remediation struct { Recommendation struct { Text string `json:"Text,omitempty"` URL string `json:"Url,omitempty"` } `json:"Recommendation,omitempty"` } `json:"Remediation,omitempty"` ProductFields struct { ProductName string `json:"Product Name,omitempty"` } `json:"ProductFields,omitempty"` Resources []struct { Type string `json:"Type,omitempty"` ID string `json:"Id,omitempty"` Partition string `json:"Partition,omitempty"` Region string `json:"Region,omitempty"` Details struct { Container struct { ImageName string `json:"ImageName,omitempty"` } `json:"Container,omitempty"` Other struct { CVEID string `json:"CVE ID,omitempty"` CVETitle string `json:"CVE Title,omitempty"` PkgName string `json:"PkgName,omitempty"` InstalledPackage string `json:"Installed Package,omitempty"` PatchedPackage string `json:"Patched Package,omitempty"` NvdCvssScoreV3 string `json:"NvdCvssScoreV3,omitempty"` NvdCvssVectorV3 string `json:"NvdCvssVectorV3,omitempty"` NvdCvssScoreV2 string `json:"NvdCvssScoreV2,omitempty"` NvdCvssVectorV2 string `json:"NvdCvssVectorV2,omitempty"` } `json:"Other,omitempty"` } `json:"Details,omitempty"` } `json:"Resources,omitempty"` RecordState string `json:"RecordState,omitempty"` } type Report struct { Findings []Finding } type AWSSecurityHubClient struct { client securityHubAPI Name string } func (sh AWSSecurityHubClient) GetName() string { return sh.Name } func (sh *AWSSecurityHubClient) Init() error { // Load the Shared AWS Configuration (~/.aws/config) cfg, err := config.LoadDefaultConfig(context.TODO()) if err != nil { return fmt.Errorf("failed to load AWS config: %w", err) } sh.client = securityhub.NewFromConfig(cfg) if sh.client == nil { return fmt.Errorf("failed to create AWS Security Hub client") } return nil } func (sh AWSSecurityHubClient) Send(m map[string]string) error { var r Report if err := json.Unmarshal([]byte(m["description"]), &r); err != nil { return fmt.Errorf("AWS Security Hub unmarshalling failed: %w", err) } if len(r.Findings) <= 0 { return fmt.Errorf("trivy AWS sent no findings to Postee, skipping sending") } var awsfindings []types.AwsSecurityFinding for _, f := range r.Findings { af := types.AwsSecurityFinding{ AwsAccountId: aws.String(f.AwsAccountID), CreatedAt: aws.String(f.CreatedAt), Description: aws.String(f.Description), GeneratorId: aws.String(f.GeneratorID), Id: aws.String(f.ID), ProductArn: aws.String(f.ProductArn), SchemaVersion: aws.String(f.SchemaVersion), Title: aws.String(f.Title), UpdatedAt: aws.String(f.UpdatedAt), Types: f.Types, } af.Resources = append(af.Resources, []types.Resource{ { Id: aws.String(f.ID), Type: aws.String(strings.Join(f.Types, " ")), }, }...) af.Remediation = &types.Remediation{ Recommendation: &types.Recommendation{ Text: aws.String(f.Remediation.Recommendation.Text), Url: aws.String(f.Remediation.Recommendation.URL), }, } af.Severity = &types.Severity{ Label: types.SeverityLabel(f.Severity.Label), } awsfindings = append(awsfindings, af) } var successCount, failedCount int awsFindingChunks := chunkBy(awsfindings, 100) log.Printf("sending %d findings in %d chunk(s) to AWS Security Hub", len(awsfindings), len(awsFindingChunks)) for _, awsfindingChunk := range awsFindingChunks { output, err := sh.client.BatchImportFindings(context.TODO(), &securityhub.BatchImportFindingsInput{ Findings: awsfindingChunk, }) if err != nil { return fmt.Errorf("upload to AWS Security Hub failed: %w", err) } if len(output.FailedFindings) > 0 { failedCount += len(output.FailedFindings) log.Printf("%d findings failed to be reported...", len(output.FailedFindings)) for _, ff := range output.FailedFindings { log.Printf("Failed finding details: ID: %s , ErrorCode: %s, ErrorMessage: %s\n", *ff.Id, *ff.ErrorCode, *ff.ErrorMessage) } } successCount += int(output.SuccessCount) } log.Printf("successfully sent: %d findings to AWS Security Hub", successCount) return nil } func (sh AWSSecurityHubClient) Terminate() error { return nil } func (sh AWSSecurityHubClient) GetLayoutProvider() layout.LayoutProvider { // Todo: This is MOCK. Because Formatting isn't need for Webhook // todo: The App should work with `return nil` return new(formatting.HtmlProvider) } func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) { for chunkSize < len(items) { items, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize]) } return append(chunks, items) } ================================================ FILE: actions/aws_securityhub_test.go ================================================ package actions import ( "context" "fmt" "testing" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/securityhub" "github.com/aws/aws-sdk-go-v2/service/securityhub/types" "github.com/aws/smithy-go/middleware" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const GoodFindings = `{ "Findings": [ { "SchemaVersion": "2018-10-08", "Id": "alpine:3.10 (alpine 3.10.9)/CVE-2021-36159", "ProductArn": "arn:aws:securityhub:eu-west-2::product/aquasecurity/aquasecurity", "GeneratorId": "Trivy/CVE-2021-36159", "AwsAccountId": "000000", "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], "CreatedAt": "2022-08-05T22:29:18.549914-07:00", "UpdatedAt": "2022-08-10T22:29:18.549938-07:00", "Severity": { "Label": "CRITICAL" }, "Title": "Trivy found a vulnerability to CVE-2021-36159 in container alpine:3.10 (alpine 3.10.9)", "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 '\\0' terminator one byte too late.", "Remediation": { "Recommendation": { "Text": "More information on this vulnerability is provided in the hyperlink", "Url": "https://avd.aquasec.com/nvd/cve-2021-36159" } }, "ProductFields": { "Product Name": "Trivy" }, "Resources": [ { "Type": "Container", "Id": "alpine:3.10 (alpine 3.10.9)", "Partition": "aws", "Region": "", "Details": { "Container": { "ImageName": "alpine:3.10 (alpine 3.10.9)" }, "Other": { "CVE ID": "CVE-2021-36159", "CVE Title": "", "PkgName": "apk-tools", "Installed Package": "2.10.6-r0", "Patched Package": "2.10.7-r0", "NvdCvssScoreV3": "9.1", "NvdCvssVectorV3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H", "NvdCvssScoreV2": "6.4", "NvdCvssVectorV2": "AV:N/AC:L/Au:N/C:P/I:N/A:P" } } } ], "RecordState": "ACTIVE" }, { "SchemaVersion": "2018-10-08", "Id": "alpine:3.10 (alpine 3.10.9)/CVE-2021-36159", "ProductArn": "arn:aws:securityhub:eu-west-2::product/aquasecurity/aquasecurity", "GeneratorId": "Trivy/CVE-2021-36159", "AwsAccountId": "000000", "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], "CreatedAt": "2022-08-05T22:29:18.549914-07:00", "UpdatedAt": "2022-08-10T22:29:18.549938-07:00", "Severity": { "Label": "CRITICAL" }, "Title": "Trivy found a vulnerability to CVE-2021-36159 in container alpine:3.10 (alpine 3.10.9)", "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 '\\0' terminator one byte too late.", "Remediation": { "Recommendation": { "Text": "More information on this vulnerability is provided in the hyperlink", "Url": "https://avd.aquasec.com/nvd/cve-2021-36159" } }, "ProductFields": { "Product Name": "Trivy" }, "Resources": [ { "Type": "Container", "Id": "alpine:3.10 (alpine 3.10.9)", "Partition": "aws", "Region": "", "Details": { "Container": { "ImageName": "alpine:3.10 (alpine 3.10.9)" }, "Other": { "CVE ID": "CVE-2021-36159", "CVE Title": "", "PkgName": "apk-tools", "Installed Package": "2.10.6-r0", "Patched Package": "2.10.7-r0", "NvdCvssScoreV3": "9.1", "NvdCvssVectorV3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H", "NvdCvssScoreV2": "6.4", "NvdCvssVectorV2": "AV:N/AC:L/Au:N/C:P/I:N/A:P" } } } ], "RecordState": "ACTIVE" } ] }` type mockAWSSHClient struct { securityHubAPI batchImportFindingsFunc func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) } func (mc mockAWSSHClient) BatchImportFindings(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) { if mc.batchImportFindingsFunc != nil { return mc.batchImportFindingsFunc(ctx, params, optFns...) } return &securityhub.BatchImportFindingsOutput{}, nil } func TestAWSSecurityHubClient_Send(t *testing.T) { t.Run("happy path, multiple findings", func(t *testing.T) { ac := AWSSecurityHubClient{ client: &mockAWSSHClient{ batchImportFindingsFunc: func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) { t.Helper() assert.Equal(t, 2, len(params.Findings)) return &securityhub.BatchImportFindingsOutput{ SuccessCount: 2, }, nil }, }, } require.NoError(t, ac.Send(map[string]string{ "description": GoodFindings, }), t.Name()) }) t.Run("happy path, no findings", func(t *testing.T) { ac := AWSSecurityHubClient{ client: &mockAWSSHClient{ batchImportFindingsFunc: func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) { t.Helper() assert.Fail(t, "this method should not have been called") return nil, nil }, }, } require.Equal(t, "trivy AWS sent no findings to Postee, skipping sending", ac.Send(map[string]string{ "description": `{"Findings":[]}`, }).Error(), t.Name()) }) t.Run("sad path, bad incoming event from trivy", func(t *testing.T) { require.Equal(t, "AWS Security Hub unmarshalling failed: invalid character 'i' looking for beginning of value", AWSSecurityHubClient{}.Send(map[string]string{ "description": "invalid json", }).Error()) }) t.Run("sad path, aws security hub fails has an error", func(t *testing.T) { ac := AWSSecurityHubClient{ client: &mockAWSSHClient{ batchImportFindingsFunc: func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) { t.Helper() return &securityhub.BatchImportFindingsOutput{}, fmt.Errorf("internal server error") }, }, } require.Equal(t, "upload to AWS Security Hub failed: internal server error", ac.Send(map[string]string{ "description": GoodFindings, }).Error(), t.Name()) }) t.Run("sad path, aws security hub fails to ingest some findings", func(t *testing.T) { ac := AWSSecurityHubClient{ client: &mockAWSSHClient{ batchImportFindingsFunc: func(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error) { t.Helper() return &securityhub.BatchImportFindingsOutput{ FailedCount: 1, SuccessCount: 1, FailedFindings: []types.ImportFindingsError{ { ErrorCode: aws.String("123"), ErrorMessage: aws.String("bad bad"), Id: aws.String("001"), }, }, ResultMetadata: middleware.Metadata{}, }, nil }, }, } require.NoError(t, ac.Send(map[string]string{ "description": GoodFindings, }), t.Name()) }) } ================================================ FILE: actions/dependencytrack.go ================================================ package actions import ( "context" "encoding/base64" "encoding/json" "fmt" "log" "strings" dtrack "github.com/DependencyTrack/client-go" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) type DependencyTrackAction struct { Name string Url string APIKey string } func (dta *DependencyTrackAction) GetName() string { return dta.Name } func (dta *DependencyTrackAction) Init() error { log.Printf("Starting Dependency Track action %s, for sending to %s", dta.Name, dta.Url) return nil } func (dta *DependencyTrackAction) Send(content map[string]string) error { project, ok := content["title"] if !ok && project == "" { return fmt.Errorf("title key not found") } projectAndVersion := strings.SplitN(project, ":", 2) if len(projectAndVersion) != 2 { return fmt.Errorf("title key has wrong format") } bom, err := json.Marshal(json.RawMessage(content["description"])) if err != nil { return fmt.Errorf("description key has wrong format: %w", err) } client, err := dtrack.NewClient(dta.Url, dtrack.WithAPIKey(dta.APIKey)) if err != nil { return fmt.Errorf("failed to create dependency track client: %w", err) } ctx := context.Background() _, err = client.BOM.Upload(ctx, dtrack.BOMUploadRequest{ ProjectName: projectAndVersion[0], ProjectVersion: projectAndVersion[1], AutoCreate: true, BOM: base64.StdEncoding.EncodeToString(bom), }) if err != nil { return fmt.Errorf("failed to upload BOM: %w", err) } log.Printf("successfully sent: %q to Dependency Track", dta.Name) return nil } func (dta *DependencyTrackAction) Terminate() error { log.Printf("Dependency Track action %s terminated.", dta.Name) return nil } func (dta *DependencyTrackAction) GetLayoutProvider() layout.LayoutProvider { return new(formatting.HtmlProvider) } ================================================ FILE: actions/dependencytrack_test.go ================================================ package actions import ( "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestDependencyTrackAction_Send(t *testing.T) { bomJSON := `{ "$schema": "http://cyclonedx.org/schema/bom-1.4.schema.json", "bomFormat": "CycloneDX", "specVersion": "1.4", "serialNumber": "urn:uuid:78f7eeb2-25fd-45ce-9ece-63cf0ca9b1af", "version": 1, "metadata": { "timestamp": "2023-07-26T07:42:41+00:00", "tools": [ { "vendor": "aquasecurity", "name": "trivy", "version": "0.43.1" } ], "component": { "bom-ref": "pkg:oci/busybox@sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a?repository_url=index.docker.io%2Flibrary%2Fbusybox\u0026arch=arm64", "type": "container", "name": "busybox:latest", "purl": "pkg:oci/busybox@sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a?repository_url=index.docker.io%2Flibrary%2Fbusybox\u0026arch=arm64", "properties": [ { "name": "aquasecurity:trivy:DiffID", "value": "sha256:57d0c5e3b21e4fdac106cfee383d702b92cd433e6e45588153228670b616bc59" }, { "name": "aquasecurity:trivy:ImageID", "value": "sha256:d38589532d9756ff743d2149a143bfad79833261ff18c24b22088183a651ff65" }, { "name": "aquasecurity:trivy:RepoDigest", "value": "busybox@sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a" }, { "name": "aquasecurity:trivy:RepoTag", "value": "busybox:latest" }, { "name": "aquasecurity:trivy:SchemaVersion", "value": "2" } ] } }, "components": [], "dependencies": [ { "ref": "pkg:oci/busybox@sha256:caa382c432891547782ce7140fb3b7304613d3b0438834dce1cad68896ab110a?repository_url=index.docker.io%2Flibrary%2Fbusybox\u0026arch=arm64", "dependsOn": [] } ], "vulnerabilities": [] }` type fields struct { Name string Url string APIKey string } type args struct { content map[string]string } tests := []struct { name string fields fields args args wantErr string errMsg string handlerFunc http.HandlerFunc }{ { name: "valid content JSON BOM", fields: fields{ Name: "test", APIKey: "key", }, args: args{ content: map[string]string{ "title": "test-project:test-version", "description": bomJSON, }, }, handlerFunc: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, err := w.Write([]byte(`{"token":"6026693d-b182-4569-8ba1-0b2c0cc509be"}`)) if err != nil { panic(err) } }), }, { name: "not found title", fields: fields{ Name: "test", APIKey: "key", }, args: args{ content: map[string]string{ "description": bomJSON, }, }, wantErr: "title key not found", }, { name: "invalid title format", fields: fields{ Name: "test", APIKey: "key", }, args: args{ content: map[string]string{ "title": "invalid", "description": bomJSON, }, }, wantErr: "title key has wrong format", }, { name: "invalid description format", fields: fields{ Name: "test", APIKey: "key", }, args: args{ content: map[string]string{ "title": "test-project:test-version", "description": "invalid", }, }, wantErr: "description key has wrong format: json: error calling MarshalJSON for type json.RawMessage: invalid character 'i' looking for beginning of value", handlerFunc: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }), }, { name: "failed to upload BOM", fields: fields{ Name: "test", APIKey: "invalid", }, args: args{ content: map[string]string{ "title": "test-project:test-version", "description": bomJSON, }, }, wantErr: "failed to upload BOM: api error (status: 401)", handlerFunc: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusUnauthorized) }), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ts := httptest.NewServer(tt.handlerFunc) defer ts.Close() url := tt.fields.Url if url == "" { url = ts.URL } dta := &DependencyTrackAction{ Name: tt.fields.Name, Url: url, APIKey: tt.fields.APIKey, } err := dta.Send(tt.args.content) if tt.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), tt.wantErr) } else { assert.NoError(t, err, tt.name) } }) } } ================================================ FILE: actions/docker.go ================================================ package actions import ( "bytes" "context" "encoding/json" "fmt" "log" "strings" "github.com/tidwall/gjson" "github.com/aquasecurity/postee/v2/layout" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" "github.com/google/uuid" ) type DockerClient struct { client client.APIClient uuidNew func() uuid.UUID Name string ImageName string Cmd []string Volumes map[string]string Network string Env []string } func (d DockerClient) GetName() string { return d.Name } func (d *DockerClient) Init() error { var err error d.client, err = client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return fmt.Errorf("failed to initialize docker action: %w", err) } d.uuidNew = uuid.New log.Println("docker action successfully initialized") return nil } func (d DockerClient) Send(m map[string]string) error { ctx := context.Background() parsedCmd := d.parseCmd(m) r, err := d.client.ImagePull(ctx, d.ImageName, types.ImagePullOptions{}) if err != nil { return fmt.Errorf("docker action failed to pull docker image: %w", err) } defer r.Close() var hc container.HostConfig if len(d.Volumes) > 0 { for src, dst := range d.Volumes { hc.Mounts = append(hc.Mounts, mount.Mount{Type: mount.TypeBind, Source: src, Target: dst}) } } if len(d.Network) > 0 { hc.NetworkMode = container.NetworkMode(d.Network) } env := append(d.Env, fmt.Sprintf(`POSTEE_EVENT="%s"`, m["description"])) ctrName := fmt.Sprintf("postee-%s-%s", d.GetName(), d.uuidNew()) _, err = d.client.ContainerCreate(ctx, &container.Config{ Image: d.ImageName, Cmd: parsedCmd, Env: env, }, &hc, nil, nil, ctrName) if err != nil { return fmt.Errorf("docker action failed to create docker container: %w", err) } defer func() { _ = d.client.ContainerRemove(ctx, ctrName, types.ContainerRemoveOptions{Force: true}) }() if err := d.client.ContainerStart(ctx, ctrName, types.ContainerStartOptions{}); err != nil { return fmt.Errorf("docker action failed to start container: %w", err) } statusCh, errCh := d.client.ContainerWait(ctx, ctrName, container.WaitConditionNotRunning) select { case err := <-errCh: if err != nil { return fmt.Errorf("docker action failed running container: %w", err) } case <-statusCh: } out, err := d.client.ContainerLogs(ctx, ctrName, types.ContainerLogsOptions{ ShowStdout: true}) if err != nil { return fmt.Errorf("docker action unable to fetch container logs: %w", err) } var buf bytes.Buffer _, _ = stdcopy.StdCopy(&buf, &buf, out) log.Println("docker action ran successfully, container logs: ", buf.String()) return nil } func (d DockerClient) Terminate() error { if err := d.client.Close(); err != nil { return fmt.Errorf("docker action unable to terminate: %w", err) } log.Println("docker action terminated successfully") return nil } func (d DockerClient) GetLayoutProvider() layout.LayoutProvider { return nil } func (d DockerClient) parseCmd(input map[string]string) (parsedCmds []string) { for _, c := range d.Cmd { var calcVal string if strings.HasPrefix(c, regoInputPrefix) { if ok := json.Valid([]byte(input["description"])); ok { // input is json calcVal = gjson.Get(input["description"], strings.TrimPrefix(c, regoInputPrefix+".")).String() } else { calcVal = input["description"] // input is a string } } else { calcVal = c // no rego to parse } parsedCmds = append(parsedCmds, calcVal) } return } ================================================ FILE: actions/docker_test.go ================================================ package actions import ( "context" "fmt" "io" "net/http" "strings" "testing" "github.com/docker/docker/api/types" containertypes "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" networktypes "github.com/docker/docker/api/types/network" "github.com/docker/docker/client" "github.com/google/uuid" specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" ) type mockDockerClient struct { client.APIClient imagePull func(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) containerCreate func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) containerStart func(ctx context.Context, container string, options types.ContainerStartOptions) error containerWait func(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.ContainerWaitOKBody, <-chan error) containerLogs func(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) containerRemove func(ctx context.Context, container string, options types.ContainerRemoveOptions) error } func (m mockDockerClient) ImagePull(ctx context.Context, ref string, options types.ImagePullOptions) (io.ReadCloser, error) { if m.imagePull != nil { return m.imagePull(ctx, ref, options) } return io.NopCloser(strings.NewReader(`pulling image foo bar`)), nil } func (m mockDockerClient) ContainerCreate(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) { if m.containerCreate != nil { return m.containerCreate(ctx, config, hostConfig, networkingConfig, platform, containerName) } return containertypes.ContainerCreateCreatedBody{ ID: "foo-bar-123", }, nil } func (m mockDockerClient) ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error { if m.containerStart != nil { return m.containerStart(ctx, container, options) } return nil } func (m mockDockerClient) ContainerWait(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.ContainerWaitOKBody, <-chan error) { if m.containerWait != nil { return m.containerWait(ctx, container, condition) } resultC := make(chan containertypes.ContainerWaitOKBody) errC := make(chan error) go func() { resultC <- containertypes.ContainerWaitOKBody{ Error: nil, StatusCode: http.StatusOK, } errC <- nil }() return resultC, errC } func (m mockDockerClient) ContainerLogs(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) { if m.containerLogs != nil { return m.containerLogs(ctx, container, options) } return io.NopCloser(strings.NewReader("the logs of joy")), nil } func (m mockDockerClient) ContainerRemove(ctx context.Context, container string, options types.ContainerRemoveOptions) error { if m.containerRemove != nil { return m.containerRemove(ctx, container, options) } return nil } type mockUUID struct { } func (mockUUID) New() uuid.UUID { return uuid.MustParse("1471d64a-6c64-4527-bbd8-7bc772678db8") } func TestDocketClient_Send(t *testing.T) { testCases := []struct { name string inputEvent string inputDockerCmd []string imagePullFunc func(context.Context, string, types.ImagePullOptions) (io.ReadCloser, error) containerCreateFunc func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) containerRemoveFunc func(ctx context.Context, container string, options types.ContainerRemoveOptions) error containerWaitFunc func(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.ContainerWaitOKBody, <-chan error) containerStartFunc func(ctx context.Context, container string, options types.ContainerStartOptions) error containerLogsFunc func(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) expectedError string expectedLogs string }{ { name: "happy path, string input event", inputEvent: `foo bar baz`, expectedLogs: "the logs of joy", containerCreateFunc: func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) { assert.Equal(t, containertypes.Config{ Image: "docker.io/library/alpine", Cmd: []string{"echo", "hello world"}, Env: []string{"FOO=bar", `POSTEE_EVENT="foo bar baz"`}, }, *config) assert.Equal(t, containertypes.HostConfig{ Mounts: []mount.Mount{{Type: mount.TypeBind, Source: "foo-src", Target: "bar-dst"}}, NetworkMode: "host", }, *hostConfig) assert.Contains(t, containerName, "postee-my-docker-action") return containertypes.ContainerCreateCreatedBody{ ID: "foo-bar-123", }, nil }, containerRemoveFunc: func(ctx context.Context, container string, options types.ContainerRemoveOptions) error { assert.Equal(t, "postee-my-docker-action-1471d64a-6c64-4527-bbd8-7bc772678db8", container) return nil }, }, { name: "happy path, relative json input event", inputEvent: `{"hostname":"foo.host"}`, inputDockerCmd: []string{"kubectl", "delete", "pod", "event.input.hostname"}, expectedLogs: "the logs of joy", containerCreateFunc: func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) { assert.Equal(t, containertypes.Config{ Image: "docker.io/library/alpine", Cmd: []string{"kubectl", "delete", "pod", "foo.host"}, Env: []string{"FOO=bar", `POSTEE_EVENT="{"hostname":"foo.host"}"`}, }, *config) assert.Equal(t, containertypes.HostConfig{ Mounts: []mount.Mount{{Type: mount.TypeBind, Source: "foo-src", Target: "bar-dst"}}, NetworkMode: "host", }, *hostConfig) assert.Contains(t, containerName, "postee-my-docker-action") return containertypes.ContainerCreateCreatedBody{ ID: "foo-bar-123", }, nil }, containerRemoveFunc: func(ctx context.Context, container string, options types.ContainerRemoveOptions) error { assert.Equal(t, "postee-my-docker-action-1471d64a-6c64-4527-bbd8-7bc772678db8", container) return nil }, }, { name: "sad path, ImagePull returns an error", imagePullFunc: func(ctx context.Context, s string, options types.ImagePullOptions) (io.ReadCloser, error) { return nil, fmt.Errorf("failed to pull image") }, expectedError: "docker action failed to pull docker image: failed to pull image", }, { name: "sad path, ContainerCreate returns an error", containerCreateFunc: func(ctx context.Context, config *containertypes.Config, hostConfig *containertypes.HostConfig, networkingConfig *networktypes.NetworkingConfig, platform *specs.Platform, containerName string) (containertypes.ContainerCreateCreatedBody, error) { return containertypes.ContainerCreateCreatedBody{}, fmt.Errorf("container creation failed") }, expectedError: "docker action failed to create docker container: container creation failed", }, { name: "sad path, ContainerStart returns an error", containerStartFunc: func(ctx context.Context, container string, options types.ContainerStartOptions) error { return fmt.Errorf("failed to start") }, expectedError: "docker action failed to start container: failed to start", }, { name: "sad path, ContainerWait returns an error", containerWaitFunc: func(ctx context.Context, container string, condition containertypes.WaitCondition) (<-chan containertypes.ContainerWaitOKBody, <-chan error) { errC := make(chan error) go func() { errC <- fmt.Errorf("failed to wait") }() return nil, errC }, expectedError: "docker action failed running container: failed to wait", }, { name: "sad path, ContainerLogs returns an error", containerLogsFunc: func(ctx context.Context, container string, options types.ContainerLogsOptions) (io.ReadCloser, error) { return nil, fmt.Errorf("failed to get logs") }, expectedError: "docker action unable to fetch container logs: failed to get logs", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { dc := DockerClient{ Name: "my-docker-action", ImageName: "docker.io/library/alpine", Env: []string{"FOO=bar"}, Network: "host", Volumes: map[string]string{ "foo-src": "bar-dst", }, client: &mockDockerClient{ imagePull: tc.imagePullFunc, containerCreate: tc.containerCreateFunc, containerRemove: tc.containerRemoveFunc, containerWait: tc.containerWaitFunc, containerStart: tc.containerStartFunc, containerLogs: tc.containerLogsFunc, }, uuidNew: mockUUID{}.New, } switch { case tc.inputDockerCmd != nil: dc.Cmd = tc.inputDockerCmd default: dc.Cmd = []string{"echo", "hello world"} } err := dc.Send(map[string]string{"description": tc.inputEvent}) if tc.expectedError != "" { assert.Equal(t, tc.expectedError, err.Error(), tc.name) } else { assert.NoError(t, err, tc.name) } }) } } ================================================ FILE: actions/email.go ================================================ package actions import ( "crypto/tls" "errors" "fmt" "log" "net" "net/smtp" "strconv" "strings" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) var ( errThereIsNoRecipient = errors.New("there is no recipient") lookupMXFunc = net.LookupMX ) type EmailAction struct { Name string User string Password string Host string Port int Sender string Recipients []string ClientHostName string UseMX bool sendFunc func(addr string, a smtp.Auth, from string, to []string, msg []byte) error } func (email *EmailAction) GetName() string { return email.Name } func (email *EmailAction) Init() error { log.Printf("Starting Email action %q...", email.Name) if email.Sender == "" { email.Sender = email.User } if email.ClientHostName != "" { log.Printf("Action %q uses a custom client name %q instead of `localhost`", email.Name, email.ClientHostName) email.sendFunc = email.sendEmailWithCustomClient } else { email.sendFunc = smtp.SendMail } return nil } func (email *EmailAction) Terminate() error { log.Printf("Email action terminated\n") return nil } func (email *EmailAction) GetLayoutProvider() layout.LayoutProvider { return new(formatting.HtmlProvider) } func (email *EmailAction) Send(content map[string]string) error { subject := content["title"] body := content["description"] port := strconv.Itoa(email.Port) recipients := getHandledRecipients(email.Recipients, &content, email.Name) if len(recipients) == 0 { return errThereIsNoRecipient } msg := fmt.Sprintf( "To: %s\r\n"+ "From: %s\r\n"+ "Subject: %s\r\n"+ "Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n", strings.Join(recipients, ","), email.Sender, subject, body) if email.UseMX { email.sendViaMxServers(port, msg, recipients) return nil } var auth smtp.Auth if len(email.Password) > 0 && len(email.User) > 0 { auth = smtp.PlainAuth("", email.User, email.Password, email.Host) } err := email.sendFunc(email.Host+":"+port, auth, email.Sender, recipients, []byte(msg)) if err != nil { log.Println("SendMail Error:", err) log.Printf("From: %q, to %v via %q", email.Sender, email.Recipients, email.Host) return err } log.Println("Email was sent successfully!") return nil } // sendEmailWithCustomClient replaces smtp.SendMail() in cases // where it is necessary to establish a custom client host name instead of "localhost", // while keeping the remaining behavior unchanged. func (email EmailAction) sendEmailWithCustomClient(addr string, a smtp.Auth, from string, to []string, msg []byte) error { log.Printf("Sending an email via Custom client for action %q", email.Name) c, err := smtp.Dial(addr) if err != nil { return err } defer c.Close() if err := c.Hello(email.ClientHostName); err != nil { return err } if ok, _ := c.Extension("STARTTLS"); ok { config := &tls.Config{ServerName: email.Host} if err = c.StartTLS(config); err != nil { return err } } if a != nil { if err = c.Auth(a); err != nil { return err } } if err = c.Mail(from); err != nil { return err } for _, addr := range to { if err = c.Rcpt(addr); err != nil { return err } } w, err := c.Data() if err != nil { return err } _, err = w.Write(msg) if err != nil { return err } err = w.Close() if err != nil { return err } return c.Quit() } func (email EmailAction) sendViaMxServers(port string, msg string, recipients []string) { for _, rcpt := range recipients { at := strings.LastIndex(rcpt, "@") if at < 0 { log.Printf("%q isn't email", rcpt) continue } host := rcpt[at+1:] mxs, err := lookupMXFunc(host) if err != nil { log.Println("error looking up mx host: ", err) continue } for _, mx := range mxs { if err := email.sendFunc(mx.Host+":"+port, nil, email.Sender, recipients, []byte(msg)); err != nil { log.Printf("SendMail error to %q via %q", rcpt, mx.Host) log.Println("error: ", err) continue } log.Printf("The message to %q was sent successful via %q!", rcpt, mx.Host) break } } } ================================================ FILE: actions/email_test.go ================================================ package actions import ( "fmt" "net" "net/smtp" "testing" "github.com/stretchr/testify/assert" ) func mockSend(errToReturn error, emailSent *int) (func(string, smtp.Auth, string, []string, []byte) error, *emailRecorder) { r := new(emailRecorder) return func(addr string, a smtp.Auth, from string, to []string, msg []byte) error { *r = emailRecorder{addr, a, from, to, msg} if errToReturn == nil { *emailSent++ } return errToReturn }, r } type emailRecorder struct { addr string auth smtp.Auth from string to []string msg []byte } func TestEmailAction_Send(t *testing.T) { testCases := []struct { name string lookupMXFunc func(name string) ([]*net.MX, error) emailAction *EmailAction expectedMessage string sendError error expectedError error expectedSentEmails int }{ { name: "happy path, with auth, server supports auth", expectedMessage: fmt.Sprintf("To: anything@fubar.com\r\n" + "From: sender@mailer.com\r\n" + "Subject: email subject\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "\r\n" + "foo bar baz body\r\n"), expectedSentEmails: 1, }, { name: "happy path, use multiple mx servers, no auth", lookupMXFunc: func(name string) ([]*net.MX, error) { return []*net.MX{ { Host: "127.0.0.1", }, { Host: "128.0.0.1", }, }, nil }, expectedMessage: fmt.Sprintf("To: anything@fubar.com\r\n" + "From: sender@mailer.com\r\n" + "Subject: email subject\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "\r\n" + "foo bar baz body\r\n"), expectedSentEmails: 1, }, { name: "sad path, no recipients", emailAction: &EmailAction{Recipients: []string{}}, expectedError: errThereIsNoRecipient, }, { name: "sad path, client uses AUTH, smtp server does not support AUTH", sendError: fmt.Errorf("smtp: server doesn't support AUTH"), expectedError: fmt.Errorf("smtp: server doesn't support AUTH"), expectedMessage: "", expectedSentEmails: 0, }, { name: "sad path, use mx server, invalid recipient,", emailAction: &EmailAction{ Name: "my-email", User: "user", Password: "pass", Host: "127.0.0.1", Port: 587, Sender: "sender@mailer.com", Recipients: []string{"invalid recipient"}, UseMX: true, }, expectedSentEmails: 0, }, { name: "sad path, no mx server available", lookupMXFunc: func(name string) ([]*net.MX, error) { return []*net.MX{}, fmt.Errorf("no such host") }, expectedSentEmails: 0, }, { name: "sad path, use mx servers, error sending email", lookupMXFunc: func(name string) ([]*net.MX, error) { return []*net.MX{ { Host: "127.0.0.1", }, }, nil }, expectedMessage: fmt.Sprintf("To: anything@fubar.com\r\n" + "From: sender@mailer.com\r\n" + "Subject: email subject\r\n" + "Content-Type: text/html; charset=UTF-8\r\n" + "\r\n" + "foo bar baz body\r\n"), sendError: fmt.Errorf("internal server error"), expectedSentEmails: 0, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var eo EmailAction if tc.emailAction != nil { eo = *tc.emailAction } else { eo = EmailAction{ Name: "my-email", User: "user", Password: "pass", Host: "127.0.0.1", Port: 587, Sender: "sender@mailer.com", Recipients: []string{"anything@fubar.com"}, } } var emailsSent int f, r := mockSend(tc.sendError, &emailsSent) eo.sendFunc = f if tc.lookupMXFunc != nil { oldLookupMXFunc := lookupMXFunc lookupMXFunc = tc.lookupMXFunc defer func() { lookupMXFunc = oldLookupMXFunc }() eo.UseMX = true } err := eo.Send(map[string]string{"description": "foo bar baz body", "title": "email subject"}) switch { case tc.expectedError != nil: assert.Equal(t, tc.expectedError, err, tc.name) assert.Equal(t, tc.expectedSentEmails, emailsSent, tc.name) default: assert.NoError(t, err, tc.name) assert.Equal(t, tc.expectedSentEmails, emailsSent, tc.name) assert.Equal(t, tc.expectedMessage, string(r.msg), tc.name) } }) } } ================================================ FILE: actions/example/exec/defectdojo-curl-upload-scan.sh ================================================ #!/usr/bin/env sh # this shell script is meant to be executed by a Aquasec/Postee "exec" # action, the event data is passed in through environment variable # POSTEE_EVENT # # Requirements on JSON format # --------------------------- # - JSON dictionary with "defectdojo" as top-level key # - "defectdojo" dictionary holds at least 2 keys # - "scan", containing the report # - "metadata", containing key/value pairs # # Required parameter # ------------------ # - DEFECTDOJO_URL - Defectdojo URL, base URL, script appends path for v2 # - DEFECTDOJO_API_TOKEN # - POSTEE_EVENT - variable containing the JSON content from template stage TEMP_PREFIX="/tmp/dd-scan-" if [ -z "$DEFECTDOJO_API_TOKEN" ]; then echo "ERROR: could not find environment variable DEFECTDOJO_API_TOKEN" exit 1 fi if [ -z "$DEFECTDOJO_URL" ]; then echo "could not find environment variable DEFECTDOJO_URL" exit 1 fi if [ -z "$POSTEE_EVENT" ]; then echo "could not read any input data from POSTEE_EVENT" exit 1 fi # shellcheck disable=SC2317 # used in signal trap for EXIT _cleanup() { rm -f "${TEMP_PREFIX}*" } trap _cleanup EXIT # write a temporary file with content received from POSTEE_EVENT TMP_FILE="$(mktemp ${TEMP_PREFIX}XXXXXX)" _validate_json() { if echo "$POSTEE_EVENT" | jq '.defectdojo.scan' | grep 'null' 1>/dev/null; then echo "ERROR => JSON, unexpected structure \"defectdojo\"" return 1 fi } if ! _validate_json; then exit 1 fi echo "$POSTEE_EVENT" | jq '.defectdojo.scan' | tee "$TMP_FILE" # Initialize the command string COMMAND="curl -X POST -H \"Authorization: Token $DEFECTDOJO_API_TOKEN\"" # extract all key/value pairs from metadata key # convert the resulting dictionary into multiline # string => $key=$value, can further be consumed # in a FOR loop generating a FORM entry per row FORM_ENTRIES=$(echo "$POSTEE_EVENT" | jq '.defectdojo.metadata | keys_unsorted[] as $k | "\($k)=\( .[$k])"') # to be able to ignore whitespaces in values, # separator for FOR loops is configured to # a newline character, remove unset IFS OLD_IFS="$IFS" # shellcheck disable=SC3003 IFS=$'\n' for entry in $FORM_ENTRIES; do COMMAND="$COMMAND -F $entry" done IFS="$OLD_IFS" DD_IMPORT_URL="${DEFECTDOJO_URL}/api/v2/import-scan/" # add URL and final JSON payload (trivy report) COMMAND="$COMMAND -F \"file=@${TMP_FILE}\" ${DD_IMPORT_URL}" if ! eval "$COMMAND"; then echo "ERROR: failed to send scan-report to ${DD_IMPORT_URL}" exit 1 fi echo "SUCCESS: send scan-report to ${DD_IMPORT_URL}" exit 0 ================================================ FILE: actions/exec.go ================================================ package actions import ( "fmt" "log" "os" "os/exec" "strings" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) type execCmd = func(string, ...string) *exec.Cmd type ExecClient struct { ExecCmd execCmd Name string Env []string InputFile string ExecScript string Action []byte } func (e *ExecClient) GetName() string { return e.Name } func (e *ExecClient) Init() error { e.ExecCmd = exec.Command return nil } func (e *ExecClient) Send(m map[string]string) error { envVars := os.Environ() envVars = append(envVars, e.Env...) envVars = append(envVars, fmt.Sprintf("POSTEE_EVENT=%s", m["description"])) var cmd *exec.Cmd if len(e.InputFile) > 0 { cmd = e.ExecCmd("/bin/sh", e.InputFile) cmd.Env = append(cmd.Env, envVars...) } if len(e.ExecScript) > 0 { cmd = e.ExecCmd("/bin/sh") cmd.Env = append(cmd.Env, envVars...) cmd.Stdin = strings.NewReader(e.ExecScript) } var err error if e.Action, err = cmd.CombinedOutput(); err != nil { return fmt.Errorf("error while executing script: %w, output: %s", err, string(e.Action)) } log.Println("execution output: ", "len: ", len(e.Action), "out: ", string(e.Action)) return nil } func (e *ExecClient) Terminate() error { log.Printf("Exec action %s terminated\n", e.GetName()) return nil } func (e *ExecClient) GetLayoutProvider() layout.LayoutProvider { // Todo: This is MOCK. Because Formatting isn't need for Webhook // todo: The App should work with `return nil` return new(formatting.HtmlProvider) } ================================================ FILE: actions/exec_test.go ================================================ package actions import ( "fmt" "io/ioutil" "os" "os/exec" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func fakeExecCmdFailure(command string, args ...string) *exec.Cmd { cs := []string{"-test.run=TestShellProcessFail", "--", command} cs = append(cs, args...) cmd := exec.Command(os.Args[0], cs...) cmd.Env = []string{"GO_TEST_PROCESS=1"} return cmd } func TestShellProcessFail(t *testing.T) { if os.Getenv("GO_TEST_PROCESS") != "1" { return } fmt.Fprint(os.Stderr, "failure") os.Exit(1) } func TestExecClient_Init(t *testing.T) { ec := ExecClient{} require.NoError(t, ec.Init()) } func TestExecClient_GetName(t *testing.T) { ec := ExecClient{Name: "my-exec-action"} require.NoError(t, ec.Init()) require.Equal(t, "my-exec-action", ec.GetName()) } func TestExecClient_Send(t *testing.T) { t.Run("happy path", func(t *testing.T) { f, err := ioutil.TempFile("", "TestExecClient_Send-*") require.NoError(t, err) defer func() { os.RemoveAll(f.Name()) }() _, _ = f.WriteString(`#!/bin/sh echo "foo" echo $POSTEE_EVENT echo $INPUT_ENV`) ec := ExecClient{ ExecCmd: exec.Command, InputFile: f.Name(), Env: []string{"INPUT_ENV=input foo env var"}, } require.NoError(t, ec.Send(map[string]string{ "description": "foo bar baz env variable", })) assert.Equal(t, `foo foo bar baz env variable input foo env var `, string(ec.Action)) assert.Equal(t, ec.Env, []string{"INPUT_ENV=input foo env var"}) }) t.Run("sad path - exec fails", func(t *testing.T) { ec := ExecClient{ ExecScript: `#!/bin/sh echo "foo bar baz"`, ExecCmd: fakeExecCmdFailure, } require.EqualError(t, ec.Send(map[string]string{ "description": "foo bar baz", }), "error while executing script: exit status 1, output: failure") }) } ================================================ FILE: actions/goldens/validbody.txt ================================================ foo bar baz body ================================================ FILE: actions/http.go ================================================ package actions import ( "encoding/base64" "encoding/json" "fmt" "io" "log" "net/http" "net/url" "os" "regexp" "strings" "github.com/aquasecurity/postee/v2/layout" "github.com/tidwall/gjson" ) var ( regoInputRegex = fmt.Sprintf(`(%s).*(.*)`, regoInputPrefix) ) type HTTPClient struct { Name string Client http.Client URL *url.URL Method string BodyFile string BodyContent string Headers map[string][]string } func (hc *HTTPClient) GetName() string { return hc.Name } func (hc *HTTPClient) Init() error { return nil } func (hc HTTPClient) Send(m map[string]string) error { // encode headers as base64 to conform HTTP spec // https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2 pe := base64.StdEncoding.EncodeToString([]byte(m["description"])) req, err := http.NewRequest(hc.Method, hc.URL.String(), nil) if err != nil { return fmt.Errorf("unable to initialize http request err: %w", err) } req.Header.Add("Postee-Event", pe) // preserve and transmit postee header for k, vals := range hc.Headers { for _, val := range vals { req.Header.Add(k, val) } } if len(hc.BodyFile) > 0 { bf, err := os.Open(hc.BodyFile) if err != nil { return fmt.Errorf("unable to read body file: %s, err: %w", hc.BodyFile, err) } req.Body = bf } if len(hc.BodyContent) > 0 { req.Body = io.NopCloser(strings.NewReader(parseBody(m, hc.BodyContent))) } resp, err := hc.Client.Do(req) if err != nil { log.Println("error during HTTP Client execution: ", err.Error()) return err } defer resp.Body.Close() b, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("unable to read HTTP response: %w", err) } if resp.StatusCode < 200 || resp.StatusCode > 299 { return fmt.Errorf("http status NOT OK: HTTP %d %s, response: %s", resp.StatusCode, http.StatusText(resp.StatusCode), string(b)) } log.Printf("http %s execution to url %s successful", hc.Method, hc.URL) return nil } func parseBody(inputEvent map[string]string, bodyContent string) string { re := regexp.MustCompile(regoInputRegex) subs := re.FindAllString(bodyContent, -1) if subs == nil { return bodyContent } for _, sub := range subs { if ok := json.Valid([]byte(inputEvent["description"])); ok { bodyContent = strings.Replace(bodyContent, sub, gjson.Get(inputEvent["description"], strings.TrimPrefix(sub, "event.input.")).String(), 1) } else { bodyContent = strings.Replace(bodyContent, "event.input", inputEvent["description"], 1) } } return bodyContent } func (hc HTTPClient) Terminate() error { log.Printf("HTTP action terminated\n") return nil } func (hc HTTPClient) GetLayoutProvider() layout.LayoutProvider { return nil } ================================================ FILE: actions/http_test.go ================================================ package actions import ( "io/ioutil" "net/http" "net/http/httptest" "net/url" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestHTTPClient_Init(t *testing.T) { ec := HTTPClient{} require.NoError(t, ec.Init()) } func TestHTTPClient_GetName(t *testing.T) { ec := HTTPClient{Name: "my-http-action"} require.NoError(t, ec.Init()) require.Equal(t, "my-http-action", ec.GetName()) } func TestHTTPClient_Send(t *testing.T) { testCases := []struct { name string method string inputEvent string bodyFile string bodyContent string testServerFunc http.HandlerFunc expectedError string }{ { name: "happy path method get", method: http.MethodGet, testServerFunc: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, []string{"bar", "baz"}, r.Header.Values("fookey")) assert.Empty(t, r.Header.Get("Postee-Event")) // no event sent }, }, { name: "happy path method post with body file, string input event", method: http.MethodPost, bodyFile: "goldens/validbody.txt", inputEvent: "foo bar baz header", testServerFunc: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, []string{"bar", "baz"}, r.Header.Values("fookey")) assert.Equal(t, "Zm9vIGJhciBiYXogaGVhZGVy", r.Header.Get("Postee-Event")) b, _ := ioutil.ReadAll(r.Body) assert.Equal(t, "foo bar baz body", string(b)) }, }, { name: "happy path method post with body content, string input event", method: http.MethodPost, bodyContent: "foo bar baz body", inputEvent: "foo bar baz header", testServerFunc: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, []string{"bar", "baz"}, r.Header.Values("fookey")) assert.Equal(t, "Zm9vIGJhciBiYXogaGVhZGVy", r.Header.Get("Postee-Event")) b, _ := ioutil.ReadAll(r.Body) assert.Equal(t, "foo bar baz body", string(b)) }, }, { name: "happy path method post, json input event", method: http.MethodPost, bodyFile: "goldens/validbody.txt", inputEvent: `{ "argsNum": 2 }`, testServerFunc: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, []string{"bar", "baz"}, r.Header.Values("fookey")) assert.Equal(t, "ewoJImFyZ3NOdW0iOiAyCn0=", r.Header.Get("Postee-Event")) b, _ := ioutil.ReadAll(r.Body) assert.Equal(t, "foo bar baz body", string(b)) }, }, { name: "happy path method post, with relative body content, json input event", method: http.MethodPost, bodyContent: `argsNum: event.input.argsNum eventID: event.input.eventID`, inputEvent: `{ "argsNum": 2, "eventID": "TRC-2" }`, testServerFunc: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, []string{"bar", "baz"}, r.Header.Values("fookey")) assert.Equal(t, "ewoJImFyZ3NOdW0iOiAyLAoJImV2ZW50SUQiOiAiVFJDLTIiCn0=", r.Header.Get("Postee-Event")) b, _ := ioutil.ReadAll(r.Body) assert.Equal(t, `argsNum: 2 eventID: TRC-2`, string(b)) }, }, { name: "happy path method post, with relative body content, string input event", method: http.MethodPost, bodyContent: `event1: event.input event1: event.input`, inputEvent: `"argsNum": 2, "eventID": "TRC-2"`, testServerFunc: func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, []string{"bar", "baz"}, r.Header.Values("fookey")) assert.Equal(t, "ImFyZ3NOdW0iOiAyLCAiZXZlbnRJRCI6ICJUUkMtMiI=", r.Header.Get("Postee-Event")) b, _ := ioutil.ReadAll(r.Body) assert.Equal(t, `event1: "argsNum": 2, "eventID": "TRC-2" event1: "argsNum": 2, "eventID": "TRC-2"`, string(b)) }, }, { name: "sad path method get - server unavailable", method: http.MethodGet, testServerFunc: func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("internal server error")) }, expectedError: "http status NOT OK: HTTP 500 Internal Server Error, response: internal server error", }, { name: "sad path method get - bad url", method: http.MethodGet, expectedError: `Get "path-to-nowhere": unsupported protocol scheme ""`, }, { name: "sad path, body file not found", method: http.MethodPost, bodyFile: "invalid.txt", expectedError: "unable to read body file: invalid.txt, err: open invalid.txt: no such file or directory", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var testUrl *url.URL if tc.testServerFunc != nil { ts := httptest.NewServer(tc.testServerFunc) testUrl, _ = url.Parse(ts.URL) } else { testUrl, _ = url.Parse("path-to-nowhere") } ec := HTTPClient{ URL: testUrl, Method: tc.method, Headers: map[string][]string{"fookey": {"bar", "baz"}}, BodyFile: tc.bodyFile, BodyContent: tc.bodyContent, } switch { case tc.expectedError != "": require.EqualError(t, ec.Send(map[string]string{"description": "foo bar baz header"}), tc.expectedError, tc.name) default: require.NoError(t, ec.Send(map[string]string{"description": tc.inputEvent}), tc.name) } }) } } ================================================ FILE: actions/jira.go ================================================ package actions import ( "crypto/tls" "errors" "fmt" "io/ioutil" "log" "strconv" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" "net/http" "net/url" "os" "strings" "github.com/aquasecurity/go-jira" ) const ( defaultIssueType = "Task" defaultIssuePriority = "High" defaultSprintPlugin = "com.pyxis.greenhopper.jira:gh-sprint" NotConfiguredSprintId = -1 ) type JiraAPI struct { Name string Url string User string Password string Token string TlsVerify bool Issuetype string ProjectKey string Priority string Assignee []string Description string Summary string SprintName string SprintId int FixVersions []string AffectsVersions []string Labels []string Unknowns map[string]string BoardName string boardId int boardType string } func (ctx *JiraAPI) GetName() string { return ctx.Name } func (ctx *JiraAPI) fetchBoardId(boardName string) { client, err := createClient(ctx) if err != nil { log.Printf("unable to create Jira client: %s, please check your credentials.", err) return } boardlist, _, err := client.Board.GetAllBoards(&jira.BoardListOptions{ProjectKeyOrID: ctx.ProjectKey}) if err != nil { log.Printf("failed to get boards from Jira API GetAllBoards with ProjectID %s. %s", ctx.ProjectKey, err) return } var matches int for _, board := range boardlist.Values { if board.Name == boardName { ctx.boardId = board.ID ctx.boardType = board.Type matches++ } } if matches > 1 { log.Printf("found more than one boards with name %q, working with board id %d", boardName, ctx.boardId) } else if matches == 0 { log.Printf("no boards found with name %s when getting all boards for User", boardName) return } else { log.Printf("using board ID %d with Name %q", ctx.boardId, boardName) } } func (ctx *JiraAPI) fetchSprintId(client *jira.Client) { sprints, _, err := client.Board.GetAllSprintsWithOptions(ctx.boardId, &jira.GetAllSprintsOptions{State: "active"}) if err != nil { log.Printf("failed to get active sprint for board ID %d from Jira API. %s", ctx.boardId, err) return } if len(sprints.Values) > 1 { ctx.SprintId = len(sprints.Values) - 1 log.Printf("Found more than one active sprint, using sprint id %d as the active sprint", ctx.SprintId) } else if len(sprints.Values) == 1 { if sprints.Values[0].ID != ctx.SprintId { ctx.SprintId = sprints.Values[0].ID log.Printf("using sprint id %d as the active sprint", ctx.SprintId) } } else { log.Printf("no active sprints exist in board ID %d Name %s", ctx.boardId, ctx.ProjectKey) } } func (ctx *JiraAPI) Terminate() error { log.Printf("Jira action terminated\n") return nil } func (ctx *JiraAPI) Init() error { if ctx.BoardName == "" { ctx.BoardName = fmt.Sprintf("%s board", ctx.ProjectKey) } ctx.fetchBoardId(ctx.BoardName) log.Printf("Starting Jira action %q....", ctx.Name) if len(ctx.Password) == 0 { ctx.Password = os.Getenv("JIRA_PASSWORD") } return nil } func (jira *JiraAPI) GetLayoutProvider() layout.LayoutProvider { return new(formatting.JiraLayoutProvider) } func (ctx *JiraAPI) buildTransportClient() (*http.Client, error) { if ctx.Token != "" { if !isServerJira(ctx.Url) { return nil, errors.New("Jira Cloud can't work with PAT") } if ctx.Password != "" { log.Printf("Found both Password and PAT, using PAT to authenticate.") } tp := jira.BearerTokenAuthTransport{ Token: ctx.Token, } if !ctx.TlsVerify { tp.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } return tp.Client(), nil } else { tp := jira.BasicAuthTransport{ Username: ctx.User, Password: ctx.Password, } if !ctx.TlsVerify { tp.Transport = &http.Transport{ TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, } } return tp.Client(), nil } } var createClient = func(ctx *JiraAPI) (*jira.Client, error) { tpClient, err := ctx.buildTransportClient() if err != nil { return nil, fmt.Errorf("unable to create new JIRA client. %w", err) } client, err := jira.NewClient(tpClient, ctx.Url) if err != nil { return client, fmt.Errorf("unable to create new JIRA client. %w", err) } return client, nil } func (ctx *JiraAPI) Send(content map[string]string) error { client, err := createClient(ctx) if err != nil { log.Printf("unable to create Jira client: %s", err) return err } if ctx.boardType == "scrum" { ctx.fetchSprintId(client) } metaProject, err := createMetaProject(client, ctx.ProjectKey) if err != nil { return fmt.Errorf("Failed to create meta project: %w", err) } ctx.Issuetype, err = getIssueType(ctx, metaProject) if err != nil { return fmt.Errorf("Failed to get issuetype: %w", err) } metaIssueType, err := createMetaIssueType(metaProject, ctx.Issuetype) if err != nil { return fmt.Errorf("Failed to create meta issue type: %w", err) } ctx.Summary = content["title"] ctx.Description = content["description"] type Version struct { Name string `json:"name"` } fieldsConfig, err := createFieldsConfig(ctx, client, &content) if err != nil { return fmt.Errorf("Failed to create fields config: %w", err) } issue, err := InitIssue(client, metaProject, metaIssueType, fieldsConfig, isServerJira(ctx.Url)) if err != nil { log.Printf("Failed to init issue: %s\n", err) return err } if len(ctx.Labels) > 0 { for _, l := range ctx.Labels { issue.Fields.Labels = append(issue.Fields.Labels, l) } } if len(ctx.FixVersions) > 0 { for _, v := range ctx.FixVersions { issue.Fields.FixVersions = append(issue.Fields.FixVersions, &jira.FixVersion{ Name: v, }) } } if len(ctx.AffectsVersions) > 0 { affectsVersions := []*Version{} for _, v := range ctx.AffectsVersions { affectsVersions = append(affectsVersions, &Version{ Name: v, }) } issue.Fields.Unknowns["versions"] = affectsVersions log.Printf("added %d affected versions into Versions field", len(ctx.AffectsVersions)) } i, err := ctx.openIssue(client, issue) if err != nil { log.Printf("Failed to open jira issue, %s\n", err) return err } log.Printf("Created new jira issue %s", i.ID) return nil } func (ctx *JiraAPI) openIssue(client *jira.Client, issue *jira.Issue) (*jira.Issue, error) { i, res, err := client.Issue.Create(issue) defer res.Body.Close() resp, _ := ioutil.ReadAll(res.Body) if err != nil { return nil, errors.New(string(resp)) } return i, nil } func createMetaProject(c *jira.Client, project string) (*jira.MetaProject, error) { meta, _, err := c.Issue.GetCreateMeta(project) if err != nil { return nil, fmt.Errorf("failed to get create meta : %w", err) } // get right project metaProject := meta.GetProjectWithKey(project) if metaProject == nil { return nil, fmt.Errorf("could not find project with key %s", project) } return metaProject, nil } func getIssueType(ctx *JiraAPI, metaProject *jira.MetaProject) (string, error) { if ctx.Issuetype != "" { if validateIssueType(ctx.Issuetype, metaProject) { // check IssueType from context return ctx.Issuetype, nil } else { return "", fmt.Errorf("project %q doesn't have issueType %q", metaProject.Name, ctx.Issuetype) } } else { if validateIssueType(defaultIssueType, metaProject) { // check default Issue Type return defaultIssueType, nil } if len(metaProject.IssueTypes) > 0 { // use 1st issueType from REST API return metaProject.IssueTypes[0].Name, nil } else { return "", fmt.Errorf("project %q doesn't have issueTypes", metaProject.Name) } } } func validateIssueType(issueType string, metaProject *jira.MetaProject) bool { for _, it := range metaProject.IssueTypes { // get issueTypes list from REST API if issueType == it.Name { return true } } return false } var getIssuePriority = func(ctx *JiraAPI, client *jira.Client) (string, error) { issuePriorityList, _, err := client.Priority.GetList() if err != nil { return "", err } if ctx.Priority != "" { if validateIssuePriority(ctx.Priority, issuePriorityList) { // check Priority from context return ctx.Priority, nil } else { return "", fmt.Errorf("project doesn't have issue priority %q", ctx.Priority) } } else { if validateIssuePriority(defaultIssuePriority, issuePriorityList) { // check default Priority return defaultIssuePriority, nil } else { if len(issuePriorityList) > 0 { return issuePriorityList[0].Name, nil // use 1st priority from REST API } else { return "", fmt.Errorf("project doesn't have issue priorities") } } } } func validateIssuePriority(priority string, priorityList []jira.Priority) bool { for _, p := range priorityList { if priority == p.Name { return true } } return false } func createFieldsConfig(ctx *JiraAPI, client *jira.Client, content *map[string]string) (map[string]string, error) { fields, _, err := client.Field.GetList() if err != nil { return nil, err } assignee := ctx.User if len(ctx.Assignee) > 0 { assignees := getHandledRecipients(ctx.Assignee, content, ctx.Name) if len(assignees) > 0 { assignee = assignees[0] } } ctx.Priority, err = getIssuePriority(ctx, client) if err != nil { return nil, fmt.Errorf("failed to get issue priority: %w", err) } fieldsConfig := make(map[string]string) for _, field := range fields { switch field.ID { case "issuetype": fieldsConfig[field.Name] = ctx.Issuetype case "project": fieldsConfig[field.Name] = ctx.ProjectKey case "priority": fieldsConfig[field.Name] = ctx.Priority case "assignee": fieldsConfig[field.Name] = assignee case "description": fieldsConfig[field.Name] = ctx.Description case "summary": fieldsConfig[field.Name] = ctx.Summary default: // Sprint is jira custom field. We found field.Name for sprint by plugin name. // "com.pyxis.greenhopper.jira:gh-sprint" is custom field that come bundled with Jira. // https://support.atlassian.com/jira-cloud-administration/docs/import-data-from-json if ctx.SprintId > 0 && field.Schema.Custom == defaultSprintPlugin { fieldsConfig[field.Name] = strconv.Itoa(ctx.SprintId) } } } //Add all custom fields that are unknown to fieldsConfig. Unknown are fields that are custom User defined in jira. for k, v := range ctx.Unknowns { fieldsConfig[k] = v } if len(ctx.Unknowns) > 0 { log.Printf("added %d custom fields to issue.", len(ctx.Unknowns)) } return fieldsConfig, nil } func createMetaIssueType(metaProject *jira.MetaProject, issueType string) (*jira.MetaIssueType, error) { metaIssuetype := metaProject.GetIssueTypeWithName(issueType) if metaIssuetype == nil { return nil, fmt.Errorf("could not find issuetype %s", issueType) } return metaIssuetype, nil } func InitIssue(c *jira.Client, metaProject *jira.MetaProject, metaIssuetype *jira.MetaIssueType, fieldsConfig map[string]string, useSrvApi bool) (*jira.Issue, error) { issue := new(jira.Issue) issueFields := new(jira.IssueFields) issueFields.Unknowns = make(map[string]interface{}) // map the field names the User presented to jira's internal key allFields, _ := metaIssuetype.GetAllFields() for key, value := range fieldsConfig { jiraKey, found := allFields[key] if !found { return nil, fmt.Errorf("key %s is not found in the list of fields", key) } valueType, err := metaIssuetype.Fields.String(jiraKey + "/schema/type") if err != nil { return nil, err } switch strings.ToLower(valueType) { case "array": // split value (string) into slice by delimiter elements := strings.Split(value, ",") elemType, err := metaIssuetype.Fields.String(jiraKey + "/schema/items") if err != nil { return nil, err } switch elemType { case "component": issueFields.Unknowns[jiraKey] = []jira.Component{{Name: value}} case "option": optionsMap := make([]map[string]string, 0) for _, element := range elements { optionsMap = append(optionsMap, map[string]string{"value": element}) } issueFields.Unknowns[jiraKey] = optionsMap default: if key == "Sprint" { num, err := strconv.Atoi(value) if err != nil { return nil, fmt.Errorf("Failed convert 'Sprint' value(string) to int: %w\n", err) } issueFields.Unknowns[jiraKey] = num // Due to Jira REST API behavior, needed to specify not a slice but a number. } else { issueFields.Unknowns[jiraKey] = []string{value} } } case "number": val, err := strconv.Atoi(value) if err != nil { return nil, fmt.Errorf("Failed convert '%s' value(string) to int: %w\n", key, err) } issueFields.Unknowns[jiraKey] = val // TODO: Handle Cascading Select List //case "option-with-child": // type CustomField struct { // Value string `json:"value"` // } // type CustomFieldCascading struct { // Value string `json:"value"` // Child CustomField `json:"child"` // } // // a := CustomFieldCascading{ Value: "1", Child: CustomField{Value: "a"}} case "string": issueFields.Unknowns[jiraKey] = value case "date": issueFields.Unknowns[jiraKey] = value case "datetime": issueFields.Unknowns[jiraKey] = value case "any": // Treat any as string issueFields.Unknowns[jiraKey] = value case "project": issueFields.Unknowns[jiraKey] = jira.Project{ Name: metaProject.Name, ID: metaProject.Id, } case "priority": issueFields.Unknowns[jiraKey] = jira.Priority{Name: value} case "user": var users []jira.User var resp *jira.Response var err error if useSrvApi { users, resp, err = findUserOnJiraServer(c, value) } else { users, resp, err = c.User.Find(value) } if err != nil { log.Printf("Get Jira User info error: %v", err) continue } if resp.StatusCode != http.StatusOK { log.Printf("http response failed: %q", resp.Status) continue } if len(users) == 0 { log.Printf("There is no user for %q", value) continue } issueFields.Unknowns[jiraKey] = users[0] case "issuetype": issueFields.Unknowns[jiraKey] = jira.IssueType{ Name: value, } case "option": issueFields.Unknowns[jiraKey] = jira.Option{ Value: value, } default: return nil, fmt.Errorf("Unknown issue type encountered: %s for %s", valueType, key) } } issue.Fields = issueFields return issue, nil } func findUserOnJiraServer(c *jira.Client, email string) ([]jira.User, *jira.Response, error) { req, _ := c.NewRequest("GET", fmt.Sprintf("/rest/api/2/user/search?username=%s", email), nil) users := []jira.User{} resp, err := c.Do(req, &users) if err != nil { log.Printf("%v", err) return nil, resp, err } return users, resp, nil } func isServerJira(rawUrl string) bool { jiraUrl, err := url.Parse(rawUrl) if err == nil { return !strings.HasSuffix(jiraUrl.Host, "atlassian.net") } return false } ================================================ FILE: actions/jira_test.go ================================================ package actions import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "os" "reflect" "testing" "github.com/aquasecurity/go-jira" "github.com/aquasecurity/postee/v2/formatting" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var metaIssuetype = &jira.MetaIssueType{Name: "Task", Fields: map[string]interface{}{ "issuetype": map[string]interface{}{ "name": "Issue Type", "schema": map[string]interface{}{ "type": "issuetype", }, }, "project": map[string]interface{}{ "name": "Project", "schema": map[string]interface{}{ "type": "project", }, }, "priority": map[string]interface{}{ "name": "Priority", "schema": map[string]interface{}{ "type": "priority", }, }, "description": map[string]interface{}{ "name": "Description", "schema": map[string]interface{}{ "type": "string", }, }, "summary": map[string]interface{}{ "name": "Summary", "schema": map[string]interface{}{ "type": "string", }, }, "assignee": map[string]interface{}{ "name": "Assignee", "schema": map[string]interface{}{ "type": "user", }, }, "customfield_10020": map[string]interface{}{ "name": "Sprint", "schema": map[string]interface{}{ "type": "array", "items": "json", }, }, "customfield_10021": map[string]interface{}{ "name": "Flagged", "schema": map[string]interface{}{ "type": "array", "items": "option", }, }, "components": map[string]interface{}{ "name": "Components", "schema": map[string]interface{}{ "type": "array", "items": "component", }, }, "versions": map[string]interface{}{ "name": "Affects versions", "schema": map[string]interface{}{ "type": "array", "items": "version", }, }, "customfield_10015": map[string]interface{}{ "name": "Start date", "schema": map[string]interface{}{ "type": "date", }, }, "customfield_10009": map[string]interface{}{ "name": "Actual end", "schema": map[string]interface{}{ "type": "datetime", }, }, "customfield_10001": map[string]interface{}{ "name": "Team", "schema": map[string]interface{}{ "type": "any", }, }, "customfield_10004": map[string]interface{}{ "name": "Impact", "schema": map[string]interface{}{ "type": "option", }, }, "timespent": map[string]interface{}{ "name": "Time Spent", "schema": map[string]interface{}{ "type": "number", }, }, "customfield_10052": map[string]interface{}{ "name": "No schema type", "schema": map[string]interface{}{}, }, "customfield_10053": map[string]interface{}{ "name": "No schema items", "schema": map[string]interface{}{ "type": "array", }, }, "customfield_10054": map[string]interface{}{ "name": "Bad Type", "schema": map[string]interface{}{ "type": "badType", }, }, }} var fieldList = &[]jira.Field{ {ID: "issuetype", Name: "Issue Type"}, {ID: "project", Name: "Project"}, {ID: "priority", Name: "Priority"}, {ID: "description", Name: "Description"}, {ID: "summary", Name: "Summary"}, } func TestJiraAPI_GetName(t *testing.T) { t.Run("happy path", func(t *testing.T) { jiraApi := &JiraAPI{Name: "testName"} name := jiraApi.GetName() assert.Equal(t, jiraApi.Name, name) }) } func TestJiraAPI_FetchBoardId(t *testing.T) { tests := []struct { name string boardName string boardList *jira.BoardsList wantJiraApi *JiraAPI wantError string }{ { name: "happy path (0 boards found)", boardName: "board0", boardList: &jira.BoardsList{Values: []jira.Board{{Name: "board1"}, {Name: "board2"}}}, wantJiraApi: &JiraAPI{BoardName: "board0"}, }, { name: "happy path (1 board found)", boardName: "board1", boardList: &jira.BoardsList{Values: []jira.Board{{Name: "board1", ID: 1, Type: "Scrum"}, {Name: "board2", ID: 2, Type: "Scrum"}}}, wantJiraApi: &JiraAPI{boardId: 1, BoardName: "board1", boardType: "Scrum"}, }, { name: "happy path (2 boards found)", boardName: "board2", boardList: &jira.BoardsList{Values: []jira.Board{{Name: "board2", ID: 1, Type: "Scrum"}, {Name: "board2", ID: 2, Type: "Scrum"}}}, wantJiraApi: &JiraAPI{boardId: 2, BoardName: "board2", boardType: "Scrum"}, }, { name: "sad path (Failed to create client)", boardName: "board3", wantJiraApi: &JiraAPI{BoardName: "board3"}, wantError: "Failed to create client", }, { name: "sad path (Failed to get boardList)", boardName: "board4", wantJiraApi: &JiraAPI{BoardName: "board4"}, wantError: "Failed to get boardList", }, } oldCreateClient := createClient defer func() { createClient = oldCreateClient }() for _, test := range tests { t.Run(test.name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.boardList, test.wantError))) defer ts.Close() createClient = func(ctx *JiraAPI) (*jira.Client, error) { if test.wantError == "Failed to create client" { return nil, fmt.Errorf(test.wantError) } else { return jira.NewClient(ts.Client(), ts.URL) } } jiraApi := &JiraAPI{BoardName: test.boardName} jiraApi.fetchBoardId(test.boardName) assert.Equal(t, test.wantJiraApi, jiraApi) }) } } func TestJiraAPI_FetchSprintId(t *testing.T) { tests := []struct { name string sprints *jira.SprintsList wantSprintId int }{ { name: "happy path (2 sprints found)", sprints: &jira.SprintsList{Values: []jira.Sprint{{Name: "sprint0"}, {Name: "sprint1"}}}, wantSprintId: 1, }, { name: "happy path (1 sprint found)", sprints: &jira.SprintsList{Values: []jira.Sprint{{Name: "sprint32", ID: 32}}}, wantSprintId: 32, }, { name: "happy path (0 sprints found)", sprints: &jira.SprintsList{Values: []jira.Sprint{}}, wantSprintId: NotConfiguredSprintId, }, { name: "sad path (Failed to get all sprints)", wantSprintId: NotConfiguredSprintId, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.sprints, ""))) defer ts.Close() jiraApi := &JiraAPI{SprintId: -1} client, err := jira.NewClient(ts.Client(), ts.URL) if err != nil { t.Fatalf("can't create jiraClient %v", err) } jiraApi.fetchSprintId(client) assert.Equal(t, test.wantSprintId, jiraApi.SprintId) }) } } func TestJiraAPI_InitIssue(t *testing.T) { metaProject := &jira.MetaProject{ Id: "project ID", Name: "project name", } tests := []struct { name string useSrvApi bool httpStatus int user []*jira.User fieldsConfig map[string]string wantIssueFields *jira.IssueFields wantError string }{ { name: "happy path", useSrvApi: true, httpStatus: http.StatusOK, user: []*jira.User{{Name: "User"}}, fieldsConfig: map[string]string{ "Issue Type": "Task", "Project": "Project", "Priority": "High", "Description": "Description", "Summary": "Summary", "Assignee": "Assignee", "Sprint": "1", "Flagged": "Flagged", "Components": "Components", "Affects versions": "1.0.1", "Start date": "01.01.2022", "Actual end": "01.01.2222", "Team": "Team", "Impact": "Impact", "Time Spent": "10", }, wantIssueFields: &jira.IssueFields{Unknowns: map[string]interface{}{ "issuetype": jira.IssueType{Name: "Task"}, "project": jira.Project{ Name: "project name", ID: "project ID", }, "priority": jira.Priority{Name: "High"}, "assignee": jira.User{Name: "User"}, "description": "Description", "summary": "Summary", "customfield_10020": 1, "customfield_10021": []map[string]string{{"value": "Flagged"}}, "components": []jira.Component{{Name: "Components"}}, "versions": []string{"1.0.1"}, "customfield_10015": "01.01.2022", "customfield_10009": "01.01.2222", "customfield_10001": "Team", "customfield_10004": jira.Option{Value: "Impact"}, "timespent": 10, }}, }, { name: "happy path (useSrvApi = false)", useSrvApi: false, httpStatus: http.StatusOK, user: []*jira.User{{Name: "User"}}, fieldsConfig: map[string]string{ "Assignee": "Assignee", }, wantIssueFields: &jira.IssueFields{Unknowns: map[string]interface{}{ "assignee": jira.User{Name: "User"}, }}, }, { name: "happy path (find user returns error)", httpStatus: http.StatusOK, fieldsConfig: map[string]string{ "Assignee": "Assignee", }, wantIssueFields: &jira.IssueFields{Unknowns: map[string]interface{}{}}, }, { name: "happy path (users not found)", httpStatus: http.StatusOK, user: []*jira.User{}, fieldsConfig: map[string]string{ "Assignee": "Assignee", }, wantIssueFields: &jira.IssueFields{Unknowns: map[string]interface{}{}}, }, { name: "sad path (bad field in fieldsConfig)", fieldsConfig: map[string]string{"Bad-field": "bad-field"}, wantError: "key Bad-field is not found in the list of fields", }, { name: "sad path (field doesn't have schema/type)", fieldsConfig: map[string]string{"No schema type": "No schema type"}, wantError: "\"customfield_10052/schema/type\" is not set", }, { name: "sad path (field doesn't have schema/items)", fieldsConfig: map[string]string{"No schema items": "No schema items"}, wantError: "\"customfield_10053/schema/items\" is not set", }, { name: "sad path (sprint is not a number)", fieldsConfig: map[string]string{"Sprint": "one"}, wantError: "strconv.Atoi: parsing \"one\": invalid syntax", }, { name: "sad path (number field is not a number)", fieldsConfig: map[string]string{"Time Spent": "two"}, wantError: "strconv.Atoi: parsing \"two\": invalid syntax", }, { name: "sad path (bad field type)", fieldsConfig: map[string]string{"Bad Type": "Bad Type"}, wantError: "Unknown issue type encountered", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.user, test.wantError))) defer ts.Close() jiraClient, err := jira.NewClient(ts.Client(), ts.URL) if err != nil { t.Fatalf("can't create jiraClient %v", err) } issue, err := InitIssue(jiraClient, metaProject, metaIssuetype, test.fieldsConfig, test.useSrvApi) if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { assert.Equal(t, test.wantIssueFields, issue.Fields) } }) } } func TestJiraAPI_GetLayoutProvider(t *testing.T) { t.Run("happy path", func(t *testing.T) { jiraApi := &JiraAPI{} wantLayoutProviderType := new(formatting.JiraLayoutProvider) LayoutProvider := jiraApi.GetLayoutProvider() assert.Equal(t, reflect.TypeOf(wantLayoutProviderType), reflect.TypeOf(LayoutProvider)) }) } func TestJiraAPI_BuildTransportClient(t *testing.T) { tests := []struct { name string jiraApi *JiraAPI wantTransport interface{} wantError string }{ { name: "happy path bearer auth", jiraApi: &JiraAPI{Token: "token", Password: "password"}, wantTransport: &jira.BearerTokenAuthTransport{}, }, { name: "happy path bearer auth", jiraApi: &JiraAPI{User: "User", Password: "Password"}, wantTransport: &jira.BasicAuthTransport{}, }, { name: "sad path bearer auth for server jira", jiraApi: &JiraAPI{Token: "token", Url: "https://johndoe.atlassian.net"}, wantError: "Jira Cloud can't work with PAT", }, { name: "sad path bearer auth with bad url", jiraApi: &JiraAPI{Token: "token", Url: "https:// johndoe.atlassian.net"}, wantError: "Jira Cloud can't work with PAT", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { client, err := test.jiraApi.buildTransportClient() if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { assert.Equal(t, reflect.TypeOf(test.wantTransport), reflect.TypeOf(client.Transport)) } }) } } func TestJiraApi_createClient(t *testing.T) { tests := []struct { name string jiraApi *JiraAPI wantError string }{ { name: "happy path", jiraApi: &JiraAPI{}, }, { name: "sad path (using PAT for cloud jira)", jiraApi: &JiraAPI{Token: "token", Url: "https://johndoe.atlassian.net"}, wantError: "Jira Cloud can't work with PAT", }, { name: "sad path (bad url)", jiraApi: &JiraAPI{Url: "https://johndoe .atlassian.net"}, wantError: "unable to create new JIRA client", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := createClient(test.jiraApi) if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { require.Nil(t, err) } }) } } func TestJiraAPI_Send(t *testing.T) { tests := []struct { name string jiraApi *JiraAPI createMetaInfo *jira.CreateMetaInfo fieldList *[]jira.Field priorityList *[]jira.Priority issue *jira.Issue serverInfo *jira.JiraServerInfo content map[string]string wantError string }{ { name: "happy path", jiraApi: &JiraAPI{ ProjectKey: "project", User: "user", boardType: "scrum", Labels: []string{"label1", "label2"}, FixVersions: []string{"fix1", "fix2"}, AffectsVersions: []string{"affect1", "affect2"}, }, createMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: "project", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}}, fieldList: fieldList, priorityList: &[]jira.Priority{{Name: "High"}}, issue: &jira.Issue{}, serverInfo: &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}}, content: map[string]string{"title": "title_content", "description": "description_content"}, }, { name: "sad path (Failed to create client)", wantError: "Failed to create client", }, { name: "sad path (Failed to create meta project)", jiraApi: &JiraAPI{}, wantError: "Failed to create meta project", }, { name: "sad path (Failed to get issuetype)", jiraApi: &JiraAPI{Issuetype: "bogusIssueType", ProjectKey: "project"}, serverInfo: &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}}, createMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: "project", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}}, wantError: "Failed to get issuetype", }, { name: "sad path (Failed to create fields config)", jiraApi: &JiraAPI{ProjectKey: "project"}, serverInfo: &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}}, createMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: "project", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}}, wantError: "Failed to create fields config", }, { name: "sad path (Failed to init issue)", jiraApi: &JiraAPI{ProjectKey: "project", Unknowns: map[string]string{"bad field": "bad field"}}, serverInfo: &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}}, createMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: "project", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}}, fieldList: fieldList, priorityList: &[]jira.Priority{{Name: "High"}}, wantError: "key bad field is not found in the list of fields", }, { name: "sad path (Failed to open issue)", jiraApi: &JiraAPI{ProjectKey: "project"}, serverInfo: &jira.JiraServerInfo{VersionNumbers: []int{8, 3, 0}}, createMetaInfo: &jira.CreateMetaInfo{Projects: []*jira.MetaProject{{Key: "project", IssueTypes: []*jira.MetaIssueType{metaIssuetype}}}}, fieldList: fieldList, priorityList: &[]jira.Priority{{Name: "High"}}, wantError: "Failed to open issue", }, } oldCreateClient := createClient defer func() { createClient = oldCreateClient }() for _, test := range tests { t.Run(test.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/rest/api/2/issue/createmeta/", buildHttpHandler(test.createMetaInfo, test.wantError)) mux.HandleFunc("/rest/api/2/field", buildHttpHandler(test.fieldList, test.wantError)) mux.HandleFunc("/rest/api/2/priority", buildHttpHandler(test.priorityList, test.wantError)) mux.HandleFunc("/rest/api/2/issue", buildHttpHandler(test.issue, test.wantError)) mux.HandleFunc("/rest/api/2/serverInfo", buildHttpHandler(test.serverInfo, test.wantError)) ts := httptest.NewServer(mux) defer ts.Close() createClient = func(ctx *JiraAPI) (*jira.Client, error) { if test.wantError == "Failed to create client" { return nil, fmt.Errorf(test.wantError) } else { return jira.NewClient(ts.Client(), ts.URL) } } err := test.jiraApi.Send(test.content) if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { require.NoError(t, err) } }) } } func TestJiraAPI_OpenIssue(t *testing.T) { tests := []struct { name string issue *jira.Issue wantError string }{ { name: "Happy path", issue: &jira.Issue{ID: "issue1"}, }, { name: "sad path", wantError: "open issue error", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.issue, test.wantError))) defer ts.Close() jiraApi := &JiraAPI{} jiraClient, err := jira.NewClient(ts.Client(), ts.URL) if err != nil { t.Fatalf("can't create jiraClient %v", err) } issue, err := jiraApi.openIssue(jiraClient, test.issue) if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { assert.Equal(t, test.issue, issue) } }) } } func TestJiraAPI_CreateMetaProject(t *testing.T) { tests := []struct { name string metaInfo *jira.CreateMetaInfo wantMetaProjectKey string wantError string }{ { name: "happy path", metaInfo: &jira.CreateMetaInfo{ Projects: []*jira.MetaProject{ {Key: "test"}, {Key: "debug"}, }, }, wantMetaProjectKey: "debug", }, { name: "sad path (jira return error)", wantError: "failed to get create meta", }, { name: "sad path (project not found)", metaInfo: &jira.CreateMetaInfo{ Projects: []*jira.MetaProject{ {Key: "test"}, {Key: "debug"}, }, }, wantMetaProjectKey: "non-existent-project", wantError: "could not find project with key non-existent-project", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("/rest/api/2/issue/createmeta/", buildHttpHandler(test.metaInfo, test.wantError)) mux.HandleFunc("/rest/api/2/serverInfo", buildHttpHandler(&jira.JiraServerInfo{VersionNumbers: []int{8, 4, 0}}, test.wantError)) ts := httptest.NewServer(mux) defer ts.Close() jiraClient, err := jira.NewClient(ts.Client(), ts.URL) if err != nil { t.Fatalf("can't create jiraClient %v", err) } metaProject, err := createMetaProject(jiraClient, test.wantMetaProjectKey) if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { require.Equal(t, test.wantMetaProjectKey, metaProject.Key) } }) } } func TestJiraAPI_CreateIssueType(t *testing.T) { tests := []struct { name string jiraAPI *JiraAPI metaProject *jira.MetaProject wantIssueType string wantError string }{ { name: "happy path (empty issueType, jira has 'Task' field)", jiraAPI: &JiraAPI{}, metaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: "Task"}, {Name: "Bug"}}}, wantIssueType: "Task", }, { name: "happy path (empty issueType, jira doesn't have 'Task' field)", jiraAPI: &JiraAPI{}, metaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: "Story"}, {Name: "Bug"}}}, wantIssueType: "Story", }, { name: "happy path (fill issueType, jira has 'Bug' field)", jiraAPI: &JiraAPI{Issuetype: "Bug"}, metaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: "Task"}, {Name: "Bug"}}}, wantIssueType: "Bug", }, { name: "bad path (fill issueType, jira doesn't have 'Bug' field)", jiraAPI: &JiraAPI{Issuetype: "Bug"}, metaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: "Task"}, {Name: "Story"}}}, wantError: "project \"\" doesn't have issueType \"Bug\"", }, { name: "bad path (metaIssueType has empty IssueTypes)", jiraAPI: &JiraAPI{Priority: ""}, metaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{}}, wantError: "project \"\" doesn't have issueTypes", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { issueType, err := getIssueType(test.jiraAPI, test.metaProject) if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { require.Equal(t, test.wantIssueType, issueType) } }) } } func TestJiraAPI_GetIssuePriority(t *testing.T) { tests := []struct { name string jiraAPI *JiraAPI priorities []*jira.Priority wantPriority string wantError string }{ { name: "happy path (empty priority, jira has 'High' field)", jiraAPI: &JiraAPI{Priority: ""}, priorities: []*jira.Priority{{Name: "Highest"}, {Name: "High"}, {Name: "Medium"}}, wantPriority: "High", }, { name: "happy path (empty priority, jira doesn't have 'High' field)", jiraAPI: &JiraAPI{Priority: ""}, priorities: []*jira.Priority{{Name: "Highest"}, {Name: "Low"}, {Name: "Medium"}}, wantPriority: "Highest", }, { name: "happy path (fill priority, jira has 'Medium' field)", jiraAPI: &JiraAPI{Priority: "Medium"}, priorities: []*jira.Priority{{Name: "Highest"}, {Name: "High"}, {Name: "Medium"}}, wantPriority: "Medium", }, { name: "bad path (fill priority, jira doesn't have 'Medium' field)", jiraAPI: &JiraAPI{Priority: "Medium"}, priorities: []*jira.Priority{{Name: "Highest"}, {Name: "High"}, {Name: "Low"}}, wantError: "project doesn't have issue priority \"Medium\"", }, { name: "bad path (jira returns empty priorities)", jiraAPI: &JiraAPI{Priority: ""}, priorities: []*jira.Priority{}, wantError: "project doesn't have issue priorities", }, { name: "bad path (jira returns error)", jiraAPI: &JiraAPI{Priority: ""}, wantError: "json: cannot unmarshal object into Go value of type []jira.Priority", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.priorities, test.wantError))) defer ts.Close() jiraClient, err := jira.NewClient(ts.Client(), ts.URL) if err != nil { t.Fatalf("can't create jiraClient %v", err) } priority, err := getIssuePriority(test.jiraAPI, jiraClient) if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { require.Equal(t, test.wantPriority, priority) } }) } } func TestJiraAPI_CreateFieldsConfig(t *testing.T) { tests := []struct { name string fields []*jira.Field jiraApi *JiraAPI content *map[string]string wantFieldsConfig map[string]string wantError string }{ { name: "happy path (default field names)", fields: []*jira.Field{ {ID: "issuetype", Name: "Issue Type"}, {ID: "project", Name: "Project"}, {ID: "priority", Name: "Priority"}, {ID: "assignee", Name: "Assignee"}, {ID: "description", Name: "Description"}, {ID: "summary", Name: "Summary"}, }, jiraApi: &JiraAPI{ User: "User", Issuetype: "Task", ProjectKey: "Project", Priority: "High", Description: "Description", Summary: "Summary", }, content: &map[string]string{}, wantFieldsConfig: map[string]string{ "Issue Type": "Task", "Project": "Project", "Priority": "High", "Assignee": "User", "Description": "Description", "Summary": "Summary", }, }, { name: "happy path (custom field names)", fields: []*jira.Field{ {ID: "issuetype", Name: "Custom Issue Type"}, {ID: "project", Name: "Custom Project"}, {ID: "priority", Name: "Custom Priority"}, {ID: "assignee", Name: "Custom Assignee"}, {ID: "description", Name: "Custom Description"}, {ID: "summary", Name: "Custom Summary"}, {ID: "customfield_10020", Name: "Custom Sprint", Schema: jira.FieldSchema{Custom: defaultSprintPlugin}}, }, jiraApi: &JiraAPI{ User: "User", Issuetype: "Task", ProjectKey: "Project", Priority: "High", Description: "Description", Summary: "Summary", SprintId: 432, Assignee: []string{"Assignee"}, }, content: &map[string]string{"owners": "owners"}, wantFieldsConfig: map[string]string{ "Custom Issue Type": "Task", "Custom Project": "Project", "Custom Priority": "High", "Custom Assignee": "Assignee", "Custom Description": "Description", "Custom Summary": "Summary", "Custom Sprint": "432", }, }, { name: "happy path (custom fields)", fields: []*jira.Field{ {ID: "issuetype", Name: "Issue Type"}, {ID: "project", Name: "Project"}, {ID: "priority", Name: "Priority"}, {ID: "assignee", Name: "Assignee"}, {ID: "description", Name: "Description"}, {ID: "summary", Name: "Summary"}, }, jiraApi: &JiraAPI{ User: "User", Issuetype: "Task", ProjectKey: "Project", Priority: "High", Description: "Description", Summary: "Summary", Unknowns: map[string]string{"Custom field": "Custom field value"}, }, content: &map[string]string{}, wantFieldsConfig: map[string]string{ "Issue Type": "Task", "Project": "Project", "Priority": "High", "Assignee": "User", "Description": "Description", "Summary": "Summary", "Custom field": "Custom field value", }, }, { name: "sad path (filed.GetList() return error)", wantError: "json: cannot unmarshal string into Go value of type []jira.Field", }, { name: "sad path (createIssuePriority return error)", fields: []*jira.Field{}, jiraApi: &JiraAPI{}, wantError: "project doesn't have issue priorities", }, } oldGetIssuePriority := getIssuePriority defer func() { getIssuePriority = oldGetIssuePriority }() for _, test := range tests { t.Run(test.name, func(t *testing.T) { ts := httptest.NewServer(http.HandlerFunc(buildHttpHandler(test.fields, test.wantError))) defer ts.Close() jiraClient, err := jira.NewClient(ts.Client(), ts.URL) if err != nil { t.Fatalf("can't create jiraClient %v", err) } getIssuePriority = func(ctx *JiraAPI, client *jira.Client) (string, error) { if test.wantError != "" { return "", fmt.Errorf(test.wantError) } else { return test.jiraApi.Priority, nil } } fieldsConfig, err := createFieldsConfig(test.jiraApi, jiraClient, test.content) if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { assert.Equal(t, test.wantFieldsConfig, fieldsConfig) } }) } } func TestJiraAPI_CreateMetaIssueType(t *testing.T) { tests := []struct { name string metaProject *jira.MetaProject issueType string wantMetaIssueType *jira.MetaIssueType wantError string }{ { name: "happy path", metaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: "Task"}, {Name: "Bug"}}}, wantMetaIssueType: &jira.MetaIssueType{Name: "Task"}, }, { name: "sad path", metaProject: &jira.MetaProject{IssueTypes: []*jira.MetaIssueType{{Name: "SubTask"}, {Name: "Bug"}}}, wantError: "could not find issuetype", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { metaIssueType, err := createMetaIssueType(test.metaProject, defaultIssueType) if test.wantError != "" { require.NotNil(t, err) assert.Contains(t, err.Error(), test.wantError) } else { assert.Equal(t, test.wantMetaIssueType, metaIssueType) } }) } } func TestJiraAPI_Init(t *testing.T) { tests := []struct { name string jiraApi *JiraAPI envPassword string wantJiraApi *JiraAPI }{ { name: "happy path", jiraApi: &JiraAPI{BoardName: "board0", ProjectKey: "project", Password: "password"}, wantJiraApi: &JiraAPI{BoardName: "board0", ProjectKey: "project", Password: "password"}, }, { name: "happy path (empty BoardName)", jiraApi: &JiraAPI{ProjectKey: "project", Password: "password"}, wantJiraApi: &JiraAPI{BoardName: "project board", ProjectKey: "project", Password: "password"}, }, { name: "happy path(empty password)", jiraApi: &JiraAPI{BoardName: "board0", ProjectKey: "project"}, envPassword: "test_password", wantJiraApi: &JiraAPI{BoardName: "board0", ProjectKey: "project", Password: "test_password"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if test.envPassword != "" { savedJiraPassword := os.Getenv("JIRA_PASSWORD") _ = os.Setenv("JIRA_PASSWORD", test.envPassword) defer func() { _ = os.Setenv("JIRA_PASSWORD", savedJiraPassword) }() } _ = test.jiraApi.Init() assert.Equal(t, test.wantJiraApi, test.jiraApi) }) } } func buildHttpHandler(successResponse interface{}, errorResponse string) func(w http.ResponseWriter, r *http.Request) { if !reflect.ValueOf(successResponse).IsNil() { // successResponse always has type therefore != nil (https://go.dev/doc/faq#nil_error) return func(w http.ResponseWriter, r *http.Request) { fieldListJson, _ := json.Marshal(successResponse) _, _ = w.Write(fieldListJson) } } else { return func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNotFound) if errorResponse != "" { _, _ = w.Write([]byte(errorResponse)) } } } } ================================================ FILE: actions/kubernetes.go ================================================ package actions import ( "context" "encoding/json" "fmt" "log" "os" "strings" "github.com/aquasecurity/postee/v2/layout" "github.com/tidwall/gjson" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/util/retry" ) const ( regoInputPrefix = "event.input" KubernetesLabelKey = "labels" KubernetesAnnotationKey = "annotations" ) func IsK8s() bool { _, ok := os.LookupEnv("KUBERNETES_SERVICE_HOST") return ok } func updateMap(old map[string]string, new map[string]string) map[string]string { newMap := make(map[string]string) for k, v := range old { newMap[k] = v } for k, v := range new { newMap[k] = v } return newMap } type KubernetesClient struct { clientset kubernetes.Interface Name string KubeNamespace string KubeConfigFile string KubeLabelSelector string KubeActions map[string]map[string]string } func (k KubernetesClient) GetName() string { return k.Name } func (k *KubernetesClient) Init() error { config, err := clientcmd.BuildConfigFromFlags("", k.KubeConfigFile) if err != nil { log.Println("unable to initialize kubernetes config: ", err) return err } k.clientset, err = kubernetes.NewForConfig(config) if err != nil { log.Println("unable to initialize kubernetes client: ", err) return err } return nil } func jsonOrString(input map[string]string, filter string) string { var ret string if json.Valid([]byte(input["description"])) { // input is json ret = gjson.Get(input["description"], filter).String() } else { ret = input["description"] // input is a string } return ret } func (k KubernetesClient) prepareInputs(input map[string]string) (string, map[string]map[string]string) { retAction := make(map[string]map[string]string) var retLabelSelector string retLabelSelector = k.KubeLabelSelector if strings.Contains(k.KubeLabelSelector, regoInputPrefix) { retLabelSelector = jsonOrString(input, strings.TrimPrefix(k.KubeLabelSelector, regoInputPrefix+".")) } for key, m := range k.KubeActions { for id, val := range m { var calcVal string if strings.HasPrefix(val, regoInputPrefix) { calcVal = jsonOrString(input, strings.TrimPrefix(val, regoInputPrefix+".")) } else { calcVal = val // no rego to parse } if _, ok := retAction[key][id]; !ok && len(retAction[key]) == 0 { retAction[key] = map[string]string{id: calcVal} } else { retAction[key][id] = calcVal } } } return retLabelSelector, retAction } func (k KubernetesClient) Send(m map[string]string) error { ctx := context.Background() labelSelector, actions := k.prepareInputs(m) // TODO: Allow configuring of resource {pod, ds, ...} pods, _ := k.clientset.CoreV1().Pods(k.KubeNamespace).List(ctx, metav1.ListOptions{ LabelSelector: labelSelector, }) for _, pod := range pods.Items { if len(actions[KubernetesLabelKey]) > 0 { retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { pod, err := k.clientset.CoreV1().Pods(pod.GetNamespace()).Get(ctx, pod.Name, metav1.GetOptions{}) if err != nil { return fmt.Errorf("failed to get updated pod for labeling: %s, err: %w", pod.Name, err) } labels := updateMap(pod.GetLabels(), actions[KubernetesLabelKey]) pod.SetLabels(labels) _, err = k.clientset.CoreV1().Pods(pod.GetNamespace()).Update(ctx, pod, metav1.UpdateOptions{}) if err != nil { log.Println("failed to apply labels to pod:", pod.Name, "err:", err.Error(), "retrying...") return err } else { log.Println("labels applied successfully to pod:", pod.Name) } return nil }) if retryErr != nil { log.Println("failed to apply labels to pod:", pod.Name, "err:", retryErr) } } if len(actions[KubernetesAnnotationKey]) > 0 { retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error { pod, err := k.clientset.CoreV1().Pods(pod.GetNamespace()).Get(ctx, pod.Name, metav1.GetOptions{}) if err != nil { return fmt.Errorf("failed to get updated pod for annotating: %s, err: %w", pod.Name, err) } annotations := updateMap(pod.GetAnnotations(), actions[KubernetesAnnotationKey]) pod.SetAnnotations(annotations) _, err = k.clientset.CoreV1().Pods(pod.GetNamespace()).Update(ctx, pod, metav1.UpdateOptions{}) if err != nil { log.Println("failed to apply annotation to pod:", pod.Name, "err:", err.Error(), "retrying...") return err } else { log.Println("annotations applied successfully to pod:", pod.Name) } return nil }) if retryErr != nil { log.Println("failed to apply annotations to pod:", pod.Name, "err:", retryErr) } } } return nil } func (k KubernetesClient) Terminate() error { log.Printf("Kubernetes output terminated\n") return nil } func (k KubernetesClient) GetLayoutProvider() layout.LayoutProvider { return nil } ================================================ FILE: actions/kubernetes_test.go ================================================ package actions import ( "context" "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" fake2 "k8s.io/client-go/kubernetes/typed/core/v1/fake" k8stesting "k8s.io/client-go/testing" ) func TestKubernetesClientSend_Labels(t *testing.T) { testCases := []struct { name string inputEvent string reactorFunc func(k8stesting.Action) (bool, runtime.Object, error) inputActions map[string]map[string]string inputLabelSelector string expectedLabels map[string]string }{ { name: "happy path, labels are added", inputEvent: `{"SigMetadata":{"ID":"TRC-2"}}`, inputActions: map[string]map[string]string{ "labels": {"foo": "bar"}, }, inputLabelSelector: "app=nginx", expectedLabels: map[string]string{ "app": "nginx", "foo": "bar", }, }, { name: "happy path, relative label selector and labels are added", inputEvent: `{"SigMetadata":{"ID":"TRC-2", "Hostname":"nginx"}}`, inputActions: map[string]map[string]string{ "labels": {"foo": "bar"}, }, inputLabelSelector: "app=event.input.SigMetadata.Hostname", expectedLabels: map[string]string{ "app": "nginx", "foo": "bar", }, }, { name: "happy path, json input event, relative input labels are added", inputEvent: `{"SigMetadata":{"ID":"TRC-2", "Hostname":"foo.com"}}`, inputActions: map[string]map[string]string{ "labels": { "foo": "event.input.SigMetadata.ID", "hostname": "event.input.SigMetadata.Hostname", }, }, inputLabelSelector: "app=nginx", expectedLabels: map[string]string{ "app": "nginx", "foo": "TRC-2", "hostname": "foo.com", }, }, { name: "happy path, string input event, relative input labels are added", inputEvent: `foo bar baz`, inputActions: map[string]map[string]string{ "labels": {"foo": "event.input"}, }, inputLabelSelector: "app=nginx", expectedLabels: map[string]string{ "app": "nginx", "foo": "foo bar baz", }, }, { name: "sad path, unable to add label", inputEvent: `{"SigMetadata":{"ID":"TRC-2"}}`, inputLabelSelector: "app=nginx", reactorFunc: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { return true, nil, fmt.Errorf("failed to update label") }, expectedLabels: map[string]string{ "app": "nginx", }, }, { name: "sad path, no matching label selector and no labels are added", inputEvent: `{"SigMetadata":{"ID":"TRC-2"}}`, inputActions: map[string]map[string]string{ "labels": {"foo": "bar"}, }, inputLabelSelector: "app=doesntexist", expectedLabels: map[string]string{ "app": "nginx", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { k := KubernetesClient{ clientset: fake.NewSimpleClientset(), KubeNamespace: "testing", KubeActions: tc.inputActions, KubeLabelSelector: tc.inputLabelSelector, } if tc.reactorFunc != nil { k.clientset.CoreV1().(*fake2.FakeCoreV1).Fake.PrependReactor("update", "pods", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { return true, nil, fmt.Errorf("failed to update label") }) } pod := &v1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", Namespace: "testing", Labels: map[string]string{"app": "nginx"}, }, } _, err := k.clientset.CoreV1().Pods("testing").Create(context.TODO(), pod, metav1.CreateOptions{}) require.NoError(t, err, tc.name) require.NoError(t, k.Send(map[string]string{"description": tc.inputEvent}), tc.name) pods, _ := k.clientset.CoreV1().Pods("testing").Get(context.TODO(), "test-pod", metav1.GetOptions{}) assert.Equal(t, tc.expectedLabels, pods.Labels, tc.name) }) } } func TestKubernetesClientSend_Annotations(t *testing.T) { testCases := []struct { name string inputEvent string reactorFunc func(k8stesting.Action) (bool, runtime.Object, error) inputActions map[string]map[string]string expectedAnnotations map[string]string }{ { name: "happy path, labels are added", inputEvent: `{"SigMetadata":{"ID":"TRC-2"}}`, inputActions: map[string]map[string]string{ "annotations": {"foo": "bar"}, }, expectedAnnotations: map[string]string{ "app": "nginx", "foo": "bar", }, }, { name: "happy path, json input event, relative input annotations are added", inputEvent: `{"SigMetadata":{"ID":"TRC-2"}}`, inputActions: map[string]map[string]string{ "annotations": {"foo": "event.input.SigMetadata.ID"}, }, expectedAnnotations: map[string]string{ "app": "nginx", "foo": "TRC-2", }, }, { name: "happy path, string input event, relative input annotations are added", inputEvent: `foo bar baz`, inputActions: map[string]map[string]string{ "annotations": {"foo": "event.input"}, }, expectedAnnotations: map[string]string{ "app": "nginx", "foo": "foo bar baz", }, }, { name: "sad path, unable to add annotations", inputEvent: `{"SigMetadata":{"ID":"TRC-2"}}`, reactorFunc: func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { return true, nil, fmt.Errorf("failed to update label") }, expectedAnnotations: map[string]string{ "app": "nginx", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { k := KubernetesClient{ clientset: fake.NewSimpleClientset(), KubeNamespace: "testing", KubeActions: tc.inputActions, } if tc.reactorFunc != nil { k.clientset.CoreV1().(*fake2.FakeCoreV1).Fake.PrependReactor("update", "pods", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { return true, nil, fmt.Errorf("failed to update annotation") }) } pod := &v1.Pod{ TypeMeta: metav1.TypeMeta{ Kind: "Pod", APIVersion: "v1", }, ObjectMeta: metav1.ObjectMeta{ Name: "test-pod", Namespace: "testing", Annotations: map[string]string{"app": "nginx"}, }, } _, err := k.clientset.CoreV1().Pods("testing").Create(context.TODO(), pod, metav1.CreateOptions{}) require.NoError(t, err, tc.name) require.NoError(t, k.Send(map[string]string{"description": tc.inputEvent}), tc.name) pods, _ := k.clientset.CoreV1().Pods("testing").Get(context.TODO(), "test-pod", metav1.GetOptions{}) assert.Equal(t, tc.expectedAnnotations, pods.Annotations, tc.name) }) } } ================================================ FILE: actions/message.go ================================================ package actions import ( "bytes" "log" "net/url" "strings" "github.com/aquasecurity/postee/v2/layout" ) const posteeDocsUrl = "https://aquasecurity.github.io/postee/settings/" func buildShortMessage(server, urls string, provider layout.LayoutProvider) string { var builder bytes.Buffer if len(server) > 0 && len(urls) > 0 { builder.WriteString(provider.P("This message is too long to display here. Please visit the link to read the content.")) links := strings.Split(urls, "\n") for _, link := range links { linkTitle, err := url.QueryUnescape(link) if err != nil { log.Printf("Query unescape error: %s", err) } builder.WriteString(provider.P(provider.A(link, linkTitle))) } } else if len(server) == 0 { builder.WriteString(provider.P("Please configure Aqua server url to get link to entire scan results.")) builder.WriteString(provider.P(provider.A(posteeDocsUrl, "Postee settings"))) } else { builder.WriteString(provider.P("Unable to create link to entire scan results. Input message doesn't contain 'registry' and 'image' fields or they are empty")) } return builder.String() } ================================================ FILE: actions/message_test.go ================================================ package actions import ( "testing" "github.com/aquasecurity/postee/v2/layout" "github.com/aquasecurity/postee/v2/formatting" "github.com/stretchr/testify/assert" ) func Test_buildShortMessage(t *testing.T) { testCases := []struct { name string provider layout.LayoutProvider inputServer string inputUrls string want string }{ { name: "happy path with slack provider", provider: new(formatting.SlackMrkdwnProvider), inputServer: "foo.com", inputUrls: "foo1.com", want: `{"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"}},`, }, { name: "happy path with teams/html provider", inputServer: "foo.com", inputUrls: "foo1.com", provider: new(formatting.HtmlProvider), want: `

This message is too long to display here. Please visit the link to read the content.

foo1.com

`, }, { name: "no configured aqua server", inputUrls: "foo1.com", provider: new(formatting.HtmlProvider), want: `

Please configure Aqua server url to get link to entire scan results.

Postee settings

`, }, { name: "no configured urls", inputServer: "foo.com", provider: new(formatting.HtmlProvider), want: `

Unable to create link to entire scan results. Input message doesn't contain 'registry' and 'image' fields or they are empty

`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got := buildShortMessage(tc.inputServer, tc.inputUrls, tc.provider) assert.Equal(t, tc.want, got, tc.name) }) } } ================================================ FILE: actions/nexusiq.go ================================================ package actions import ( "encoding/base64" "encoding/json" "fmt" "io" "io/ioutil" "log" "net/http" "regexp" "strings" "time" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) var notAllowed = regexp.MustCompile(`[\.:\/]`) func sanitizedAppName(appName string) string { return notAllowed.ReplaceAllString(appName, "_") } type NexusIqAction struct { Name string Url string User string Password string OrganizationId string } func (nexus *NexusIqAction) GetName() string { return nexus.Name } func (nexus *NexusIqAction) Init() error { log.Printf("Starting Nexus IQ action %q, for sending to %q", nexus.Name, nexus.Url) return nil } func (nexus *NexusIqAction) auth() string { return base64.StdEncoding.EncodeToString([]byte(nexus.User + ":" + nexus.Password)) } func (nexus *NexusIqAction) execute(method string, url string, payload string, headers map[string]string) (map[string]interface{}, error) { client := http.DefaultClient client.Timeout = time.Second * 120 var reader io.Reader if payload != "" { reader = strings.NewReader(payload) } req, err := http.NewRequest(method, url, reader) if err != nil { return nil, err } for name, value := range headers { req.Header.Add(name, value) } req.Header.Add("Authorization", "Basic "+nexus.auth()) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } if resp.StatusCode < 200 || resp.StatusCode > 299 { msg := "received incorrect response status: %d. Body: %s" return nil, fmt.Errorf(msg, resp.StatusCode, body) } r := make(map[string]interface{}) err = json.Unmarshal(body, &r) if err != nil { return nil, err } return r, nil } func (nexus *NexusIqAction) getAppByNameAndOrg(organizationId string, appName string) (string, error) { sanitizedAppName := sanitizedAppName(appName) url := fmt.Sprintf("%s/api/v2/applications/organization/%s", nexus.Url, organizationId) r, err := nexus.execute("GET", url, "", map[string]string{"Content-Type": "application/json"}) if err != nil { return "", fmt.Errorf("error fetching application: %w", err) } applications := r["applications"].([]interface{}) for _, item := range applications { app := item.(map[string]interface{}) if app["publicId"].(string) == sanitizedAppName { return app["id"].(string), nil } } return "", nil } func (nexus *NexusIqAction) createApp(organizationId string, appName string) (string, error) { sanitizedAppName := sanitizedAppName(appName) payload := map[string]string{ "publicId": sanitizedAppName, "name": sanitizedAppName, "organizationId": organizationId, } b, err := json.Marshal(payload) if err != nil { return "", err } url := fmt.Sprintf("%s/api/v2/applications", nexus.Url) r, err := nexus.execute("POST", url, string(b), map[string]string{"Content-Type": "application/json"}) if err != nil { return "", fmt.Errorf("error creating application: %w", err) } return r["id"].(string), nil } func (nexus *NexusIqAction) createOrGetApp(appName string) (string, error) { app, err := nexus.getAppByNameAndOrg(nexus.OrganizationId, appName) if err != nil { return "", err } if app == "" { app, err = nexus.createApp(nexus.OrganizationId, appName) if err != nil { return "", err } } return app, nil } func (nexus *NexusIqAction) registerBom(appId string, bom string) error { url := fmt.Sprintf("%s/api/v2/scan/applications/%s/sources/cyclone", nexus.Url, appId) _, err := nexus.execute("POST", url, bom, map[string]string{"Content-Type": "application/xml"}) if err != nil { return fmt.Errorf("error registering bom: %w", err) } return nil } func (nexus *NexusIqAction) Send(content map[string]string) error { appId, err := nexus.createOrGetApp(content["title"]) if err != nil { return err } data := content["description"] err = nexus.registerBom(appId, data) if err != nil { return err } return nil } func (nexus *NexusIqAction) Terminate() error { log.Printf("Nexus IQ action %q terminated.", nexus.Name) return nil } func (nexus *NexusIqAction) GetLayoutProvider() layout.LayoutProvider { /*TODO come up with smaller interface that doesn't include GetLayoutProvider()*/ return new(formatting.HtmlProvider) } ================================================ FILE: actions/nexusiq_test.go ================================================ package actions import ( "fmt" "io" "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const createdAppId = "cd8fd2f4f289445b8975092e7d3045ba" func TestSanitizedAppName(t *testing.T) { testCases := []struct { name string image string application string }{{ name: "Dot", image: "alpine-3.7", application: "alpine-3_7", }, { name: "Both dot and colon", image: "all-in-one:3.5.19223", application: "all-in-one_3_5_19223", }, { name: "Slash", image: "bpdockerlab/pii-data:1_0", application: "bpdockerlab_pii-data_1_0", }} for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { appName := sanitizedAppName(tc.image) assert.Equal(t, tc.application, appName) }) } } func TestNexusiq_Init(t *testing.T) { nx := NexusIqAction{} require.NoError(t, nx.Init()) } func TestNexusiq_GetName(t *testing.T) { nx := NexusIqAction{Name: "my-nexusiq"} require.NoError(t, nx.Init()) require.Equal(t, "my-nexusiq", nx.GetName()) } func TestNexusiq_Send(t *testing.T) { organizationId := "9beee80c6fc148dfa51e8b0359ee4d4e" applicationsJson := fmt.Sprintf(` { "applications": [ { "id": "4bb67dcfc86344e3a483832f8c496419", "publicId": "alpine-3_7", "name": "MySecondApplication", "organizationId": "%s", "contactUserName": "NewAppContact" } ] } `, organizationId) createAppPld := fmt.Sprintf(`{"name":"nginx-1_7_1","organizationId":"%s","publicId":"nginx-1_7_1"}`, organizationId) testCases := []struct { name string image string applications string expctdCreateAppPld string expctdAppId string }{{ name: "Existing application", image: "alpine-3.7", applications: applicationsJson, expctdAppId: "4bb67dcfc86344e3a483832f8c496419", }, { name: "New application", image: "nginx-1.7.1", applications: applicationsJson, expctdCreateAppPld: createAppPld, expctdAppId: createdAppId, }, } b, err := ioutil.ReadFile("testdata/nexus-iq-sbom.xml") if err != nil { t.Fatal("unable to read test data %w", err) } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { input := map[string]string{ "title": tc.image, "description": string(b), } ts := configureHttp(t, tc.applications, tc.expctdCreateAppPld, tc.expctdAppId) nx := NexusIqAction{Name: "my-nexusiq", Url: ts.URL, User: "admin", Password: "admin", OrganizationId: "9beee80c6fc148dfa51e8b0359ee4d4e"} require.NoError(t, nx.Send(input)) defer ts.Close() }) } } func configureHttp(t *testing.T, applicationsJson, expctdCreateAppPld, expctdAppId string) *httptest.Server { router := mux.NewRouter() //get applications router.HandleFunc("/api/v2/applications/organization/{organization:[a-z0-9]+}", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "GET", r.Method) _, _ = w.Write([]byte(applicationsJson)) }) //create application router.HandleFunc("/api/v2/applications", func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) require.NoError(t, err) assert.Equal(t, "POST", r.Method) assert.Equal(t, expctdCreateAppPld, string(body)) _, _ = w.Write([]byte(fmt.Sprintf(`{"id":"%s"}`, createdAppId))) }) //register bom router.HandleFunc("/api/v2/scan/applications/{app:[a-z0-9]+}/sources/cyclone", func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) assert.Equal(t, "POST", r.Method) assert.Equal(t, expctdAppId, vars["app"]) _, _ = w.Write([]byte("{}")) }) return httptest.NewServer(router) } ================================================ FILE: actions/opsgenie.go ================================================ package actions import ( "context" "encoding/json" "fmt" "log" "strings" "github.com/opsgenie/opsgenie-go-sdk-v2/alert" "github.com/opsgenie/opsgenie-go-sdk-v2/client" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) const defaultPriority = alert.P3 type OpsGenieAction struct { Name string User string APIKey string Responders []string VisibleTo []string Tags []string Alias string Entity string PrioritySource string priority alert.Priority client *alert.Client } func (ops *OpsGenieAction) GetName() string { return ops.Name } func (ops *OpsGenieAction) Init() (err error) { ops.client, err = alert.NewClient(&client.Config{ ApiKey: ops.APIKey, }) if err != nil { return } if ops.PrioritySource != "" { ops.priority = alert.Priority(ops.PrioritySource) } else { ops.priority = defaultPriority } log.Printf("Starting OpsGenie action %q....", ops.Name) return nil } func getUserResponders(users []string) []alert.Responder { if len(users) == 0 { return nil } responders := []alert.Responder{} for _, user := range users { responder := alert.Responder{Type: alert.UserResponder, Username: user} responders = append(responders, responder) } return responders } func (ops *OpsGenieAction) convertResultToOpsGenie(title string, content map[string]interface{}) *alert.CreateAlertRequest { description := "" if content["description"] != nil { description = fmt.Sprint(content["description"]) } alias := ops.Alias if content["alias"] != nil { alias = fmt.Sprint(content["alias"]) } entity := ops.Entity if content["entity"] != nil { entity = fmt.Sprint(content["entity"]) } priority := ops.priority if content["priority"] != nil { priority = alert.Priority(fmt.Sprint(content["priority"])) } tags := ops.Tags if content["tags"] != nil { switch content["tags"].(type) { case []string: tags = append(tags, content["tags"].([]string)...) case string: tags = append(tags, strings.Split(content["tags"].(string), ",")...) } } return &alert.CreateAlertRequest{ Message: title, Description: description, Alias: alias, Entity: entity, Priority: priority, Tags: tags, Responders: getUserResponders(ops.Responders), VisibleTo: getUserResponders(ops.VisibleTo), } } func (ops *OpsGenieAction) Send(input map[string]string) error { data := map[string]interface{}{} if err := json.Unmarshal([]byte(input["description"]), &data); err != nil { return err } r := ops.convertResultToOpsGenie(input["title"], data) r.User = ops.User alertResult, err := ops.client.Create(context.Background(), r) if err != nil { return err } log.Printf("Sending to %q was successful: %s", ops.Name, alertResult.Result) return nil } func (*OpsGenieAction) Terminate() error { log.Println("Terminating OpsGenie Action") return nil } func (ops *OpsGenieAction) GetLayoutProvider() layout.LayoutProvider { /*TODO come up with smaller interface that doesn't include GetLayoutProvider()*/ return new(formatting.SlackMrkdwnProvider) } ================================================ FILE: actions/opsgenie_test.go ================================================ package actions import ( "testing" "github.com/opsgenie/opsgenie-go-sdk-v2/alert" "github.com/stretchr/testify/assert" ) func TestGetUserResponders(t *testing.T) { tests := []struct { name string users []string responders []alert.Responder }{ { name: "good way", users: []string{"user1", "user2"}, responders: []alert.Responder{ {Type: alert.UserResponder, Username: "user1"}, {Type: alert.UserResponder, Username: "user2"}, }, }, { name: "without users", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { got := getUserResponders(test.users) assert.Equal(t, test.responders, got) }) } } func TestConvertResultToOpsGenie(t *testing.T) { tests := []struct { name string title string data map[string]interface{} result *alert.CreateAlertRequest }{ { name: "good way", title: "all-in-one:3.5.19223", data: map[string]interface{}{ "description": "all-in-one:3.5.19223 vulnerability scan report", "alias": "all-in-one:3.5.19223", "entity": "entity", "priority": "P4", "tags": []string{"tag1", "tag2"}, }, result: &alert.CreateAlertRequest{ Message: "all-in-one:3.5.19223", Priority: alert.P4, Description: "all-in-one:3.5.19223 vulnerability scan report", Alias: "all-in-one:3.5.19223", Entity: "entity", Tags: []string{"tag1", "tag2"}, }, }, { name: "only title", title: "all-in-one:3.5.19223", data: map[string]interface{}{}, result: &alert.CreateAlertRequest{ Message: "all-in-one:3.5.19223", Priority: alert.P3, }, }, { name: "good way with tags as string", title: "all-in-one:3.5.19223", data: map[string]interface{}{ "description": "all-in-one:3.5.19223 vulnerability scan report", "alias": "all-in-one:3.5.19223", "entity": "entity", "priority": "P4", "tags": "tag1,tag2", }, result: &alert.CreateAlertRequest{ Message: "all-in-one:3.5.19223", Priority: alert.P4, Description: "all-in-one:3.5.19223 vulnerability scan report", Alias: "all-in-one:3.5.19223", Entity: "entity", Tags: []string{"tag1", "tag2"}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { ops := &OpsGenieAction{ APIKey: "anyAPIkey", } err := ops.Init() assert.NoError(t, err) r := ops.convertResultToOpsGenie(test.title, test.data) assert.Equal(t, test.result, r) }) } } ================================================ FILE: actions/pagerduty.go ================================================ package actions import ( "context" "fmt" "log" "time" "github.com/PagerDuty/go-pagerduty" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) type Clock interface { Now() time.Time } type realClock struct{} func (rc *realClock) Now() time.Time { return time.Now() } type PagerdutyClient struct { client *pagerduty.Client clock Clock Name string AuthToken string RoutingKey string } func (p *PagerdutyClient) GetName() string { return p.Name } func (p *PagerdutyClient) Init() error { if len(p.AuthToken) <= 0 { return fmt.Errorf("pagerduty auth token is required to send events") } if len(p.RoutingKey) <= 0 { return fmt.Errorf("pagerduty routing key is required to send events") } p.client = pagerduty.NewClient(p.AuthToken) p.clock = &realClock{} return nil } func (p *PagerdutyClient) Send(m map[string]string) error { ctx := context.Background() resp, err := p.client.ManageEventWithContext(ctx, &pagerduty.V2Event{ RoutingKey: p.RoutingKey, Action: "trigger", Payload: &pagerduty.V2Payload{ Summary: m["title"], // required Source: "postee", Severity: "critical", Timestamp: p.clock.Now().Format(time.RFC3339), Details: m["description"], // required }, }) if err != nil { return fmt.Errorf("failed to send event to pagerduty: %w", err) } log.Printf("successfully sent event to pagerduty, response msg: %s, status: %s", resp.Message, resp.Status) return nil } func (p *PagerdutyClient) Terminate() error { return nil } func (p *PagerdutyClient) GetLayoutProvider() layout.LayoutProvider { /*TODO come up with smaller interface that doesn't include GetLayoutProvider()*/ return new(formatting.HtmlProvider) } ================================================ FILE: actions/pagerduty_test.go ================================================ package actions import ( "fmt" "io" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/require" "github.com/PagerDuty/go-pagerduty" "github.com/stretchr/testify/assert" ) type fakeClock struct{} func (fc *fakeClock) Now() time.Time { t, _ := time.Parse(time.RFC3339, "2022-09-22T22:07:55-07:00") return t } func TestPagerdutyClient_Init(t *testing.T) { t.Run("happy path", func(t *testing.T) { require.NoError(t, (&PagerdutyClient{ Name: "my-pagerduty", AuthToken: "123456", RoutingKey: "foobarbaz", }).Init()) }) t.Run("sad path, no auth token", func(t *testing.T) { assert.Equal(t, "pagerduty auth token is required to send events", (&PagerdutyClient{ Name: "my-pagerduty", RoutingKey: "foobarbaz", }).Init().Error()) }) t.Run("sad path, no routing key", func(t *testing.T) { assert.Equal(t, "pagerduty routing key is required to send events", (&PagerdutyClient{ Name: "my-pagerduty", AuthToken: "123456", }).Init().Error()) }) } func TestPagerdutyClient_Send(t *testing.T) { testCases := []struct { name string handlerFunc http.HandlerFunc expectedError string pagerdutyClient PagerdutyClient inputEvent map[string]string }{ { name: "happy path", handlerFunc: func(writer http.ResponseWriter, request *http.Request) { b, _ := io.ReadAll(request.Body) assert.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)) _, _ = fmt.Fprint(writer, `{"status": "ok", "dedup_key": "yes", "message": "ok"}`) }, pagerdutyClient: PagerdutyClient{ Name: "my-pagerduty", AuthToken: "foo-bar-baz", RoutingKey: "123456", }, inputEvent: map[string]string{ "description": "foo bar baz details", "title": "my fancy title", }, }, { name: "sad path, pagerduty api returns an error", handlerFunc: func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusInternalServerError) }, pagerdutyClient: PagerdutyClient{ Name: "my-pagerduty", AuthToken: "foo-bar-baz", RoutingKey: "123456", }, inputEvent: map[string]string{ "description": "foo bar baz details", "title": "my fancy title", }, expectedError: "failed to send event to pagerduty: HTTP response with status code 500 does not contain Content-Type: application/json", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { ts := httptest.NewServer(tc.handlerFunc) defer ts.Close() tc.pagerdutyClient.client = pagerduty.NewClient(tc.pagerdutyClient.AuthToken, pagerduty.WithV2EventsAPIEndpoint(ts.URL)) tc.pagerdutyClient.clock = &fakeClock{} err := tc.pagerdutyClient.Send(tc.inputEvent) switch { case tc.expectedError != "": assert.Equal(t, tc.expectedError, err.Error(), tc.name) default: assert.NoError(t, err, tc.name) } }) } } ================================================ FILE: actions/plugin.go ================================================ package actions import ( "fmt" "log" "strings" "github.com/aquasecurity/postee/v2/layout" ) const ( ApplicationScopeOwner = "<%application_scope_owner%>" ) type Action interface { GetName() string Init() error Send(map[string]string) error Terminate() error GetLayoutProvider() layout.LayoutProvider } func getHandledRecipients(recipients []string, content *map[string]string, outputName string) []string { var result []string for _, r := range recipients { if r == ApplicationScopeOwner { owners, err := getAppScopeOwners(content) if err != nil { log.Printf("get application scope owners error for %q: %v", outputName, err) continue } result = append(result, owners...) } else { result = append(result, r) } } return result } func getAppScopeOwners(content *map[string]string) ([]string, error) { ownersIn, ok := (*content)["owners"] if !ok { return nil, fmt.Errorf("recipients field contains %q, but received a webhook without this data", ApplicationScopeOwner) } return strings.Split(ownersIn, ";"), nil } ================================================ FILE: actions/servicenow.go ================================================ package actions import ( "encoding/json" "log" "strconv" "time" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" servicenow "github.com/aquasecurity/postee/v2/servicenow" ) type ServiceNowAction struct { Name string User string Password string Instance string Table string layoutProvider layout.LayoutProvider } func (sn *ServiceNowAction) GetName() string { return sn.Name } func (sn *ServiceNowAction) Init() error { log.Printf("Starting ServiceNow action %q....", sn.Name) log.Printf("Your ServiceNow Table is %q on '%s.%s'", sn.Table, sn.Instance, servicenow.BaseServer) sn.layoutProvider = new(formatting.HtmlProvider) return nil } func (sn *ServiceNowAction) Send(content map[string]string) error { log.Printf("Sending via ServiceNow %q", sn.Name) // parse date date := "" if i, err := strconv.ParseInt(content["date"], 10, 64); err == nil { date = time.Unix(i, 0).Format("2006-01-02 15:04:05") } // parse severity severity := 3 // default ServiceNow value if s, err := strconv.Atoi(content["severity"]); err == nil { severity = s } d := &servicenow.ServiceNowData{ Opened: date, ShortDescription: content["title"], Caller: sn.User, Category: content["category"], Impact: severity, Urgency: severity, Subcategory: content["subcategory"], AssignedTo: content["assignedTo"], AssignmentGroup: content["assignedGroup"], WorkNotes: "[code]" + content["description"] + "[/code]", Description: content["summary"], } body, err := json.Marshal(d) if err != nil { log.Println("ServiceNow Error:", err) return err } err = servicenow.InsertRecordToTable(sn.User, sn.Password, sn.Instance, sn.Table, body) if err != nil { log.Println("ServiceNow Error:", err) return err } log.Printf("Sending via ServiceNow %q was successful!", sn.Name) return nil } func (sn *ServiceNowAction) Terminate() error { log.Printf("ServiceNow action %q terminated", sn.Name) return nil } func (sn *ServiceNowAction) GetLayoutProvider() layout.LayoutProvider { return sn.layoutProvider } ================================================ FILE: actions/slack.go ================================================ package actions import ( "bytes" "encoding/json" "errors" "log" "strings" "github.com/aquasecurity/postee/v2/data" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" slackAPI "github.com/aquasecurity/postee/v2/slack" ) const ( slackBlockLimit = 49 ) type SlackAction struct { Name string AquaServer string Url string slackLayout layout.LayoutProvider } func (slack *SlackAction) GetName() string { return slack.Name } func (slack *SlackAction) Init() error { slack.slackLayout = new(formatting.SlackMrkdwnProvider) log.Printf("Starting Slack action %q....", slack.Name) return nil } func clearSlackText(text string) string { s := strings.ReplaceAll(text, "&", "&") s = strings.ReplaceAll(s, "<", "<") s = strings.ReplaceAll(s, ">", ">") return s } func buildSlackBlock(title string, data []byte) []byte { var content bytes.Buffer content.WriteByte('{') content.WriteString("\"blocks\":") content.WriteByte('[') content.WriteString(title) content.Write(data) content.WriteByte(']') content.WriteByte('}') return content.Bytes() } func (slack *SlackAction) Send(input map[string]string) error { log.Printf("Sending via Slack %q", slack.Name) title := clearSlackText(slack.slackLayout.TitleH2(input["title"])) var body string if strings.HasSuffix(input["description"], ",") { body = strings.TrimSuffix(input["description"], ",") } else { body = input["description"] } body = clearSlackText(body) if !strings.HasPrefix(body, "[") { body = "[" + body + "]" } if !json.Valid([]byte(body)) { return errors.New("wrong template selected, choose a correct template") } rawBlock := make([]data.SlackBlock, 0) err := json.Unmarshal([]byte(body), &rawBlock) if err != nil { log.Printf("Unmarshal slack sending error: %v", err) return err } length := len(rawBlock) if length >= slackBlockLimit { message := buildShortMessage(slack.AquaServer, input["url"], slack.slackLayout) if err := slackAPI.SendToUrl(slack.Url, buildSlackBlock(title, []byte(message))); err != nil { return err } log.Printf("Sending via Slack %q was successful!", slack.Name) } else { for n := 0; n < length; { d := length - n if d >= 49 { d = 49 } cutData, _ := json.Marshal(rawBlock[n : n+d]) cutData = cutData[1 : len(cutData)-1] if err := slackAPI.SendToUrl(slack.Url, buildSlackBlock(title, cutData)); err != nil { log.Printf("Sending to %q was finished with error: %v", slack.Name, err) return err } else { log.Printf("Sending [%d/%d part] to %q was successful!", int(n/49)+1, int(length/49)+1, slack.Name) } n += d } } return nil } func (slack *SlackAction) Terminate() error { log.Printf("Slack output %q terminated", slack.Name) return nil } func (slack *SlackAction) GetLayoutProvider() layout.LayoutProvider { return slack.slackLayout } ================================================ FILE: actions/splunk.go ================================================ package actions import ( "bytes" "crypto/tls" "encoding/json" "errors" "fmt" "io/ioutil" "log" "net" "net/http" "strings" "time" "github.com/aquasecurity/postee/v2/data" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) const defaultSizeLimit = 10000 type SplunkAction struct { Name string Url string Token string EventLimit int TlsVerify bool splunkLayout layout.LayoutProvider } func (splunk *SplunkAction) GetName() string { return splunk.Name } func (splunk *SplunkAction) Init() error { splunk.splunkLayout = new(formatting.HtmlProvider) log.Printf("Starting Splunk action %q....", splunk.Name) return nil } func (splunk *SplunkAction) Send(d map[string]string) error { log.Printf("Sending a message to %q", splunk.Name) if splunk.EventLimit == 0 { splunk.EventLimit = defaultSizeLimit } if splunk.EventLimit < defaultSizeLimit { log.Printf("[WARNING] %q has a short limit %d (default %d)", splunk.Name, splunk.EventLimit, defaultSizeLimit) } if !strings.HasSuffix(splunk.Url, "/") { splunk.Url += "/" } scanInfo := new(data.ScanImageInfo) body := []byte(d["description"]) if !json.Valid([]byte(body)) { return errors.New("wrong template selected, choose a correct template") } err := json.Unmarshal(body, scanInfo) if err != nil { log.Printf("sending to %q error: %v", splunk.Name, err) return err } eventFormat := "{\"sourcetype\": \"_json\", \"event\": " constLimit := len(eventFormat) - 1 var fields []byte for { fields, err = json.Marshal(scanInfo) if err != nil { log.Printf("sending to %q error: %v", splunk.Name, err) return err } if len(fields) < splunk.EventLimit-constLimit { break } switch { case len(scanInfo.Resources) > 0: scanInfo.Resources = nil continue case len(scanInfo.Malwares) > 0: scanInfo.Malwares = nil continue case len(scanInfo.SensitiveData) > 0: scanInfo.SensitiveData = nil continue default: msg := fmt.Sprintf("Scan result for %q is large for %q , its size if %d (limit %d)", scanInfo.Image, splunk.Name, len(fields), splunk.EventLimit) log.Print(msg) return errors.New(msg) } } var buff bytes.Buffer buff.WriteString(eventFormat) buff.Write(fields) buff.WriteByte('}') req, err := http.NewRequest("POST", splunk.Url+"services/collector", &buff) if err != nil { return err } req.Header.Add("Authorization", "Splunk "+splunk.Token) client := http.Client{ // default transport with tls config added Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, TLSClientConfig: &tls.Config{InsecureSkipVerify: splunk.TlsVerify}, }, } resp, err := client.Do(req) if err != nil { return err } if resp.StatusCode != http.StatusOK { defer resp.Body.Close() b, _ := ioutil.ReadAll(resp.Body) log.Printf("Splunk sending error: failed response status %q. Body: %q", resp.Status, string(b)) return errors.New("failed response status for Splunk sending") } log.Printf("Sending a message to %q was successful!", splunk.Name) return nil } func (splunk *SplunkAction) Terminate() error { log.Printf("Splunk action %q terminated", splunk.Name) return nil } func (splunk *SplunkAction) GetLayoutProvider() layout.LayoutProvider { return splunk.splunkLayout } ================================================ FILE: actions/stdout.go ================================================ package actions import ( "fmt" "os" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) type StdoutAction struct { Name string } func (stdout StdoutAction) GetName() string { return stdout.Name } func (stdout StdoutAction) Init() error { return nil } func (stdout StdoutAction) Send(data map[string]string) error { _, err := fmt.Fprintf(os.Stdout, "%s", data["description"]) return err } func (stdout StdoutAction) Terminate() error { return nil } func (stdout StdoutAction) GetLayoutProvider() layout.LayoutProvider { return &formatting.HtmlProvider{} } ================================================ FILE: actions/teams.go ================================================ package actions import ( "encoding/json" "log" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" "github.com/aquasecurity/postee/v2/utils" msteams "github.com/aquasecurity/postee/v2/teams" ) const ( teamsSizeLimit = 18000 // 28 KB is an approximate limit for MS Teams ) type TeamsAction struct { Name string AquaServer string teamsLayout layout.LayoutProvider Webhook string } func (teams *TeamsAction) GetName() string { return teams.Name } func (teams *TeamsAction) Init() error { log.Printf("Starting MS Teams action %q....", teams.Name) teams.teamsLayout = new(formatting.HtmlProvider) return nil } func (teams *TeamsAction) Send(input map[string]string) error { log.Printf("Sending to MS Teams via %q...", teams.Name) utils.Debug("Title for %q: %q\n", teams.Name, input["title"]) utils.Debug("Url(s) for %q: %q\n", teams.Name, input["url"]) utils.Debug("Webhook for %q: %q\n", teams.Name, teams.Webhook) utils.Debug("Length of Description for %q: %d/%d\n", teams.Name, len(input["description"]), teamsSizeLimit) var body string if len(input["description"]) > teamsSizeLimit { utils.Debug("MS Team action will send SHORT message\n") body = buildShortMessage(teams.AquaServer, input["url"], teams.teamsLayout) } else { utils.Debug("MS Team action will send LONG message\n") body = input["description"] } utils.Debug("Message is: %q\n", body) escaped, err := escapeJSON(body) if err != nil { log.Printf("Error while escaping payload: %v", err) return err } err = msteams.CreateMessageByWebhook(teams.Webhook, teams.teamsLayout.TitleH2(input["title"])+escaped) if err != nil { log.Printf("TeamsAction Send Error: %v", err) return err } log.Printf("Sending to MS Teams via %q was successful!", teams.Name) return nil } func (teams *TeamsAction) Terminate() error { log.Printf("MS Teams action %q terminated", teams.Name) return nil } func (teams *TeamsAction) GetLayoutProvider() layout.LayoutProvider { return teams.teamsLayout } func escapeJSON(s string) (string, error) { b, err := json.Marshal(s) if err != nil { panic(err) } // Trim the beginning and trailing " character return string(b[1 : len(b)-1]), nil } ================================================ FILE: actions/testdata/nexus-iq-sbom.xml ================================================ musl 1.1.18-r3 MIT CVE-2019-14697 https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-14697 9.8 9.8 9.8 critical CVSS V3 CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H Upgrade package musl to version 1.1.18-r4 or above. ================================================ FILE: actions/webhook.go ================================================ package actions import ( "fmt" "io/ioutil" "log" "net/http" "strings" "time" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) type WebhookAction struct { Name string Url string Timeout string } func (webhook *WebhookAction) GetName() string { return webhook.Name } func (webhook *WebhookAction) Init() error { log.Printf("Starting Webhook action %q, for sending to %q", webhook.Name, webhook.Url) return nil } func (webhook *WebhookAction) Send(content map[string]string) error { log.Printf("Sending webhook to %q", webhook.Url) data := content["description"] //it's not supposed to work with legacy renderer client, err := newClient(webhook.Timeout) if err != nil { return err } resp, err := client.Post(webhook.Url, "application/json", strings.NewReader(data)) if err != nil { log.Printf("Sending webhook Error: %v", err) return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Printf("Sending %q Error: %v", webhook.Name, err) return err } if resp.StatusCode != http.StatusOK { msg := "Sending webhook wrong status: '%d'. Body: %s" log.Printf(msg, resp.StatusCode, body) return fmt.Errorf(msg, resp.StatusCode, body) } log.Printf("Sending Webhook to %q was successful!", webhook.Name) return nil } func (webhook *WebhookAction) Terminate() error { log.Printf("Webhook action %q terminated.", webhook.Name) return nil } func (webhook *WebhookAction) GetLayoutProvider() layout.LayoutProvider { // Todo: This is MOCK. Because Formatting isn't need for Webhook // todo: The App should work with `return nil` return new(formatting.HtmlProvider) } var newClient = func(timeout string) (http.Client, error) { if len(timeout) == 0 || timeout == "0" { timeout = "120s" } duration, err := time.ParseDuration(timeout) if err != nil { return http.Client{}, fmt.Errorf("invalid duration specified: %w", err) } return http.Client{Timeout: duration}, nil } ================================================ FILE: actions/webhook_test.go ================================================ package actions import ( "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestWebhook_GetName(t *testing.T) { webhook := WebhookAction{Name: "webhook action"} require.NoError(t, webhook.Init()) require.Equal(t, "webhook action", webhook.GetName()) } func TestWebhook_Send(t *testing.T) { type response = struct { status int text string } tests := []struct { name string webhook WebhookAction content map[string]string resp response wantErr string }{ { name: "happy path", webhook: WebhookAction{ Name: "testName", Url: "%s/testUrl/webhook", Timeout: "120s", }, content: map[string]string{ "description": "test description", }, resp: response{ status: http.StatusOK, text: "OK", }, }, { name: "sad path (timeout error)", webhook: WebhookAction{ Name: "testName", Url: `%s/testUrl/webhook`, Timeout: "0s", }, content: map[string]string{ "description": "test description", }, resp: response{ status: http.StatusRequestTimeout, text: "Timeout error", }, wantErr: "Sending webhook wrong status: '408'. Body: Timeout error", }, { name: "sad path (Bad URL error)", webhook: WebhookAction{ Name: "testName", Url: "badurl%s", Timeout: "1m", }, content: map[string]string{ "description": "test description", }, wantErr: "unsupported protocol scheme", }, } savedNewClient := newClient defer func() { newClient = savedNewClient }() for _, test := range tests { t.Run(test.name, func(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(test.resp.status) _, _ = w.Write([]byte(test.resp.text)) })) defer server.Close() test.webhook.Url = fmt.Sprintf(test.webhook.Url, server.URL) newClient = func(timeout string) (http.Client, error) { client := server.Client() return *client, nil } err := test.webhook.Send(test.content) if test.wantErr != "" { require.Error(t, err) assert.Contains(t, err.Error(), test.wantErr) } else { require.NoError(t, err) } }) } } func TestNewClient(t *testing.T) { tests := []struct { name string timeout string wantTimeout time.Duration wantError string }{ { name: "timeout 0", timeout: "0", wantTimeout: 120000000000, }, { name: "timeout 60", timeout: "60s", wantTimeout: 60000000000, }, { name: "bad timeout", timeout: "60sm", wantError: "invalid duration specified", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { client, err := newClient(test.timeout) if test.wantError != "" { assert.NotNil(t, err) require.Contains(t, err.Error(), test.wantError) } else { require.Equal(t, test.wantTimeout, client.Timeout) } }) } } ================================================ FILE: cfg.yaml ================================================ # The configuration file contains a general settings section, # routes, templates and actions sections. name: tenant # The tenant name aqua-server: # URL of Aqua Server for links. E.g. https://myserver.aquasec.com max-db-size: 1000MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited db-verify-interval: 1 # How often to check the DB size. By default, Postee checks every 1 hour # Routes are used to define how to handle an incoming message routes: - name: stdout actions: [ stdout ] template: raw-json #- name: route1 # Route name. Must be unique # input: contains(input.image, "alpine") # REGO rule to match input message against route # input-files: # Array filePaths to files with REGO rules # - Allow-Image-Name.rego # - Ignore-Image-Name.rego # - Allow-Registry.rego # - Ignore-Registry.rego # - Policy-Only-Fix-Available.rego # - Policy-Min-Vulnerability.rego # - Policy-Related-Features.rego # actions: [my-slack] # Action name (needs to be defined under "actions") which will receive the message # template: slack-template # Template name (needs to be defined under "templates") which will be used to process the message output format # plugins: # Optional plugins # aggregate-message-number: # Number of same messages to aggregate into one output message # 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 # 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 # 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 # Templates are used to format a message templates: - name: vuls-slack # Out of the box template for slack rego-package: postee.vuls.slack # Slack template REGO package (available out of the box) - name: vuls-html # Out of the box HTML template rego-package: postee.vuls.html # HTML template REGO package (available out of the box) - name: raw-html # Raw message json rego-package: postee.rawmessage.html # HTML template REGO package (available out of the box) - name: legacy # Out of the box legacy Golang template legacy-scan-renderer: html - name: legacy-slack # Legacy slack template implemented in Golang legacy-scan-renderer: slack - name: legacy-jira # Legacy jira template implemented in Golang legacy-scan-renderer: jira - name: custom-email # Example of how to use a template from a Web URL url: # URL to custom REGO file - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json - name: vuls-cyclonedx # export vulnerabilities to CycloneDX XML rego-package: postee.vuls.cyclondx - name: trivy-operator-jira rego-package: postee.trivyoperator.jira - name: trivy-operator-slack rego-package: postee.trivyoperator.slack - name: trivy-operator-dependencytrack rego-package: postee.trivyoperator.dependencytrack - name: trivy-jira rego-package: postee.trivy.jira # Rules are predefined rego policies that can be used to trigger routes rules: - name: Initial Access - name: Credential Access - name: Privilege Escalation - name: Defense Evasion - name: Persistence # Actions are target services that should consume the messages actions: - name: stdout type: stdout enable: true - name: my-jira # name must be unique type: jira # supported types: jira, email enable: false url: # Mandatory. E.g "https://johndoe.atlassian.net" user: # Mandatory. E.g :johndoe@gmail.com" password: # Optional. Specify Jira user API key. Used only for Jira Cloud token: # Optional. Specify Jira user Personal Access Token. Used only for Jira Server/Data Center project-key: # Mandatory. Specify the JIRA product key tls-verify: false board: # Optional. Specify the Jira board name to open tickets on labels: # Optional, specify array of labels to add to Ticket, for example: ["label1", "label2"] issuetype: # Optional. Specifty the issue type to open (Bug, Task, etc.). Default is "Task" priority: # Optional. Specify the issues severity. Default is "High" assignee: # Optional. Specify the assigned user. Default is the user that opened the ticket - name: my-email type: email enable: false user: # Optional (if auth supported): SMTP user name (e.g. johndoe@gmail.com) password: # Optional (if auth supported): SMTP password host: # Mandatory: SMTP host name (e.g. smtp.gmail.com) port: # Mandatory: SMTP server port (e.g. 587) sender: # Mandatory: The email address to use as a sender client-host-name: # Optional: setting the local client name instead of `localhost` recipients: ["", ""] # Mandatory: comma separated list of recipients - name: my-email-smtp-server type: email enable: false use-mx: true sender: # Mandatory: The email address to use as a sender recipients: ["", ""] # Mandatory: comma separated list of recipients - name: my-slack type: slack enable: false url: https://hooks.slack.com/services/TAAAA/BBB/ - name: ms-team type: teams enable: false url: https://outlook.office.com/webhook/.... # Webhook's url - name: webhook type: webhook enable: false url: https://..../webhook/ # Webhook's url timeout: # Webhook's timeout. pattern is used, such as "300ms" or "2h45m". Default: 120s - name: splunk type: splunk enable: false url: http://localhost:8088 # Mandatory. Url of a Splunk server token: # Mandatory. a HTTP Event Collector Token size-limit: 10000 # Optional. Maximum scan length, in bytes. Default: 10000 tls-verify: false # Enable skip TLS Verification. Default: false. - name: my-servicenow type: serviceNow enable: false user: # Mandatory. E.g :johndoe@gmail.com" password: # Mandatory. Specify user API key instance: # Mandatory. Name of ServiceN ow Instance board: # Specify the ServiceNow board name to open tickets on. Default is "incident" - name: my-nexus-iq type: nexusIq enable: false user: # Mandatory. User name password: # Mandatory. User password url: # Mandatory. Url of Nexus IQ server organization-id: # Mandatory. Organization UID like "222de33e8005408a844c12eab952c9b0" - name: my-dependencytrack type: dependencytrack enable: false url: http://localhost:8080/ # Mandatory. Url of Dependency Track server dependency-track-api-key: # Mandatory. API key of Dependency Track server - name: my-opsgenie type: opsgenie enable: false token: # Mandatory. an API key from an API integration user: # Optional. Display name of the request owner. assignee: # Optional. Comma separated list of users that the alert will be routed to send notifications recipients: [""] # Optional. Comma separated list of users that the alert will become visible to without sending any notification tags: # Optional. Comma separated list of the alert tags. priority: # Optional. Specify the alert priority. Default is "P3" alias: # Optional. Client-defined identifier of the alert. entity: # Optional. Entity field of the alert that is generally used to specify which domain alert is related to. ================================================ FILE: config/cfg-actions.yaml ================================================ # The configuration file contains a general settings section, # routes, templates and actions sections. name: tenant # The tenant name aqua-server: localhost # URL of Aqua Server for links. E.g. https://myserver.aquasec.com max-db-size: 1000MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited db-verify-interval: 1 # How often to check the DB size. By default, Postee checks every 1 hour # Routes are used to define how to handle an incoming message routes: - name: stdout actions: [ stdout ] template: raw-json - name: actions-route input: contains(input.SigMetadata.ID, "TRC-2") serialize-actions: true # Optional. Serialize actions in route. actions: [my-exec, my-exec-2, my-http-get, my-http-post-file, my-http-post-content] template: raw-json # Templates are used to format a message templates: - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json # Outputs are target services that should consume the messages actions: - name: stdout type: stdout enable: true # Define a custom output of exec action, that can take params. - name: my-exec type: exec enable: true env: ["MY_ENV_VAR=foo_bar_baz", "MY_KEY=secret"] # Optional. Any environment variables to pass in exec-script: | # Specify the script to run #!/bin/sh echo $POSTEE_EVENT - name: my-exec-2 type: exec enable: true env: ["MY_ENV_VAR=foo_bar_baz", "MY_KEY=secret"] # Optional. Any environment variables to pass in input-file: /tmp/exec-test.sh # Specify the path to the script to run - name: my-http-get type: http enable: true url: "https://my-fancy-url.com" # Required. URL of the HTTP Request method: GET # Required. Method to use. CONNECT is not supported at this time headers: # Optional. Headers to pass in for the request "Foo": ["bar", "baz"] timeout: 1s # Optional. Timeout value in XX(s,m,h) - name: my-http-post-file type: http enable: true url: "https://my-fancy-url.com" # Required. URL of the HTTP Request method: POST # Required. Method to use. CONNECT is not supported at this time body-file: /tmp/some.log.file # Optional. Body file of the HTTP request - name: my-http-post-content type: http enable: true url: "https://my-fancy-url.com" # Required. URL of the HTTP Request method: POST # Required. Method to use. CONNECT is not supported at this time headers: # Optional. Headers to pass in for the request. "Foo": [ "bar" ] "Haz": [ "baz" ] timeout: 10s # Optional. Timeout value in XX(s,m,h) body-content: | # Optional. Body inline content of the HTTP request This is an example of a inline body Event ID: event.input.Signature.ID ================================================ FILE: config/cfg-controller-runner.yaml ================================================ name: Postee Controller Runner Demo aqua-server: # URL of Aqua Server for links. E.g. https://myserver.aquasec.com max-db-size: 1000MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited db-verify-interval: 1 # How often to check the DB size. By default, Postee checks every 1 hour # Routes are used to define how to handle an incoming message routes: - name: stdout actions: [ stdout ] template: raw-json - name: controller-only-route input: contains(input.image, "alpine") actions: [my-http-post-from-controller] template: raw-json - name: runner-only-route input: contains(input.SigMetadata.ID, "TRC-1") serialize-actions: true actions: [my-exec-from-runner, my-http-post-from-runner] template: raw-json - name: controller-runner-route input: contains(input.SigMetadata.ID, "TRC-2") serialize-actions: true # Cannot be strictly guaranteed as executions happen independently on runner/controller actions: [my-exec-from-runner, my-http-post-from-runner, my-http-post-from-controller] template: raw-json # Templates are used to format a message templates: - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json # Outputs are target services that should consume the messages actions: - name: stdout type: stdout enable: true - name: my-http-post-from-controller type: http enable: true url: "https://webhook.site/" # Required. URL of the HTTP Request method: POST # Required. Method to use. CONNECT is not supported at this time headers: # Optional. Headers to pass in for the request. "Foo": [ "bar" ] timeout: 10s # Optional. Timeout value in XX(s,m,h) body-content: | # Optional. Body inline content of the HTTP request This is an example of a inline body Input Image: event.input.image - name: my-exec-from-runner runs-on: "postee-runner-1" type: exec enable: true env: ["MY_ENV_VAR=foo_bar_baz", "MY_KEY=secret"] # Optional. Any environment variables to pass in exec-script: | # Specify the script to run #!/bin/sh echo $POSTEE_EVENT echo "this is hello from postee" - name: my-http-post-from-runner runs-on: "postee-runner-1" type: http enable: true url: "https://webhook.site/" # Required. URL of the HTTP Request method: POST # Required. Method to use. CONNECT is not supported at this time body-content: | # Optional. Body inline content of the HTTP request This is an another example of a inline body Event ID: event.input.SigMetadata.ID ================================================ FILE: config/cfg-docker-actions.yaml ================================================ # The configuration file contains a general settings section, # routes, templates and actions sections. name: tenant # The tenant name aqua-server: # URL of Aqua Server for links. E.g. https://myserver.aquasec.com max-db-size: 1000MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited db-verify-interval: 1 # How often to check the DB size. By default, Postee checks every 1 hour # Routes are used to define how to handle an incoming message routes: - name: stdout actions: [ stdout ] template: raw-json - name: actions-route input: contains(input.SigMetadata.ID, "TRC-2") actions: [stop-vulnerable-pod] template: raw-json # Templates are used to format a message templates: - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json # Outputs are target services that should consume the messages actions: - name: stdout type: stdout enable: true # Define a custom output of Docker action, that can take params. - name: stop-vulnerable-pod type: docker enable: true docker-image-name: "bitnami/kubectl:latest" # Required. Image name of container to run. docker-cmd: ["delete", "pod", event.input.SigMetadata.hostname] # Required. Command to run when starting container. docker-network: "host" # Optional. Network name for docker container. docker-volume-mounts: # Optional. Volume mounts present inside the container "path/to/.kube/config": "/.kube/config" ================================================ FILE: config/cfg-k8s-actions.yaml ================================================ # The configuration file contains a general settings section, # routes, templates and actions sections. name: tenant # The tenant name aqua-server: # URL of Aqua Server for links. E.g. https://myserver.aquasec.com max-db-size: 1000MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited db-verify-interval: 1 # How often to check the DB size. By default, Postee checks every 1 hour # Routes are used to define how to handle an incoming message routes: - name: stdout actions: [ stdout ] template: raw-json - name: actions-route input: contains(input.SigMetadata.ID, "TRC-2") actions: [my-k8s] template: raw-json # Templates are used to format a message templates: - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json # Outputs are target services that should consume the messages actions: - name: stdout type: stdout enable: true # Define a custom output of k8s action, that can take params. - name: my-k8s type: kubernetes enable: true kube-namespace: "default" # Required. Kubernetes namespace to use. kube-config-file: "/path/to/kubeconfig" # Required if not running on K8S, Optional otherwise. kube-label-selector: "app=nginx-app" # Required, if specifying labels or annotations. kube-actions: labels: foo-label: "bar-value" # Required. Label to add. bar-label: event.input.SigMetadata.ID # Optional. It is also possible to add labels based on event inputs. annotations: foo-annotation: "bar-value" bar-annotation: event.input.SigMetadata.ID ================================================ FILE: config/cfg-trivy-aws.yaml ================================================ actions: - type: awssecurityhub enable: true name: Send Findings to Security Hub routes: - name: Send Trivy Findings to AWS Security Hub template: raw-json actions: - Send Findings to Security Hub input-files: - Trivy AWS Findings templates: - name: raw-json rego-package: postee.rawmessage.json rules: - name: Trivy AWS Findings name: Send Trivy Results to AWS Security Hub ================================================ FILE: config/cfg-trivy-operator-defectdojo.yaml ================================================ # The configuration file contains a general settings section, # routes, templates and actions sections. name: tenant # The tenant name aqua-server: # URL of Aqua Server for links. E.g. https://myserver.aquasec.com max-db-size: 1000MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited db-verify-interval: 1 # How often to check the DB size. By default, Postee checks every 1 hour # Routes are used to define how to handle an incoming message routes: - name: trivyScans actions: [ exec-curl-dd ] template: plejd-dd-trivy-report input: contains(input.kind, "ClusterRbacAssessmentReport") # Actions are target services that should consume the messages actions: - name: stdout type: stdout enable: false - name: exec-curl-dd # use curl to send message to defectdojo type: exec enable: true env: - "DEFECTDOJO_URL=http://webserver-http-echo:8888" input-file: /tmp/exec-curl-dd.sh # action expects shell script (plain sh shell) templates: - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json - name: plejd-dd-trivy-report # render from report into DD structure rego-package: plejd.trivyoperator.defectdojo ================================================ FILE: config/cfg-trivy-operator.yaml ================================================ routes: - name: Trivy Operator Alerts input: input.report.summary.criticalCount > 0 # You can customize this based on your needs actions: [send-slack-msg] template: trivy-operator-slack # Templates are used to format a message templates: - name: trivy-operator-slack rego-package: postee.trivyoperator.slack # Actions are target services that should consume the messages actions: - name: send-slack-msg type: slack enable: true url: ================================================ FILE: config/terminate-malicious-pods.yaml ================================================ actions: - type: webhook name: Send Message to Webhook enable: true url: http://foo.com - type: exec name: Kill Process enable: true input-file: '' exec-script: kill -9 $(echo $POSTEE_EVENT | jq .Context.hostParentProcessId) routes: - name: Notify on unauthorized access actions: - Send Message to Webhook template: raw-json action: [] input-files: - Privilege Escalation - Defense Evasion - Credential Access - Initial Access - action: [] input-files: - Defense Evasion actions: - Kill Process - Send Message to Webhook name: Terminate offending process template: raw-json templates: - name: raw-json rego-package: postee.rawmessage.json rules: - name: Initial Access - name: Credential Access - name: Privilege Escalation - name: Defense Evasion - name: Persistence - name: Tracee Default Set name: Terminating Malicious Pods ================================================ FILE: controller/controller.go ================================================ package controller import ( gotls "crypto/tls" "fmt" "io/ioutil" "log" "net" "net/url" "strconv" "time" "github.com/aquasecurity/postee/v2/router" "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go" "github.com/nats-io/nkeys" ) const ( NATSEventSubject = "postee.events" NATSConfigSubject = "postee.config" ) type Controller struct { ControllerURL string ControllerSeedFilePath string ControllerCAFile string ControllerTLSKeyPath string ControllerTLSCertPath string RunnerName string } func (c Controller) Setup(r *router.Router) error { log.Println("Running in controller mode") var configCh chan *nats.Msg var natsServer *server.Server var host string var port int if c.ControllerURL != "" { var portString string u, err := url.Parse(c.ControllerURL) if err != nil { return fmt.Errorf("invalid controller url specified: %s", c.ControllerURL) } host, portString, _ = net.SplitHostPort(u.Host) port, _ = strconv.Atoi(portString) } var err error if c.ControllerTLSKeyPath != "" && c.ControllerTLSCertPath != "" { var tlsConfig *gotls.Config tlsConfig, err = server.GenTLSConfig(&server.TLSConfigOpts{ CertFile: c.ControllerTLSCertPath, KeyFile: c.ControllerTLSKeyPath, CaFile: c.ControllerCAFile, }) if err != nil { return fmt.Errorf("invalid TLS config: %w", err) } var pubKey string var nKeys []*server.NkeyUser if c.ControllerSeedFilePath != "" { log.Println("Seedfile specified for Controller, enabling AuthN") sf, err := ioutil.ReadFile(c.ControllerSeedFilePath) if err != nil { return fmt.Errorf("unable to read seed file: %w", err) } nKey, err := nkeys.ParseDecoratedNKey(sf) if err != nil { return fmt.Errorf("unable to parse seed file: %w", err) } pubKey, err = nKey.PublicKey() if err != nil { return fmt.Errorf("unable to get public key: %w", err) } nKeys = append(nKeys, &server.NkeyUser{Nkey: pubKey}) } natsServer, err = server.NewServer(&server.Options{ TLSConfig: tlsConfig, Nkeys: nKeys, Host: host, Port: port, }) } else { natsServer, err = server.NewServer(&server.Options{Host: host, Port: port}) } if err != nil { return fmt.Errorf("unable to start controller backplane: %w", err) } go natsServer.Start() if !natsServer.ReadyForConnections(time.Second * 10) { return fmt.Errorf("controller backplane is not ready to receive connections, try restarting controller") } log.Println("Controller listening for requests on: ", natsServer.ClientURL()) configCh = make(chan *nats.Msg) var opts []nats.Option var nKeyOpt nats.Option if c.ControllerSeedFilePath != "" { nKeyOpt, err = nats.NkeyOptionFromSeed(c.ControllerSeedFilePath) if err != nil { return fmt.Errorf("unable to load seed file: %w", err) } } opts = append(opts, nKeyOpt) var nc *nats.Conn if c.ControllerCAFile != "" { opts = append(opts, nats.RootCAs(c.ControllerCAFile)) } nc, err = nats.Connect(natsServer.ClientURL(), router.SetupConnOptions(opts)...) if err != nil { return fmt.Errorf("unable to setup controller: %w", err) } log.Println("Listening to config requests on: ", NATSConfigSubject) if _, err := nc.ChanSubscribe(NATSConfigSubject, configCh); err != nil { return fmt.Errorf("unable to subscribe for config requests from runners on: %s, err: %w", NATSConfigSubject, err) } r.ConfigCh = configCh r.NatsServer = natsServer r.Mode = "controller" r.NatsMsgCh = make(chan *nats.Msg) eventSubj := NATSEventSubject log.Println("Subscribing to events from runners on: ", eventSubj) if _, err := nc.ChanSubscribe(eventSubj, r.NatsMsgCh); err != nil { return fmt.Errorf("unable to subscribe for events from runners on: %s, err: %w", eventSubj, err) } return nil } ================================================ FILE: data/inpteval.go ================================================ package data type Inpteval interface { Eval(in map[string]interface{}, serverUrl string) (map[string]string, error) BuildAggregatedContent(items []map[string]string) (map[string]string, error) IsAggregationSupported() bool } ================================================ FILE: data/slack.go ================================================ package data type SlackTextBlock struct { TypeField string `json:"type"` TextField string `json:"text"` } type SlackBlock struct { TypeField string `json:"type"` TextField *SlackTextBlock `json:"text,omitempty"` Fields []SlackTextBlock `json:"fields,omitempty"` } ================================================ FILE: data/types.go ================================================ package data type ScanImageInfo struct { Image string `json:"image"` Registry string `json:"registry"` Digest string `json:"digest"` PreviousDigest string `json:"previous_digest"` ImageAssuranceResults `json:"image_assurance_results,omitempty"` VulnerabilitySummary `json:"vulnerability_summary,omitempty"` ScanOptions `json:"scan_options,omitempty"` Resources []InfoResources `json:"resources,omitempty"` ApplicationScopeOwners []string `json:"application_scope_owners,omitempty"` Malwares []MalwareData `json:"malware,omitempty"` SensitiveData []SensitiveData `json:"sensitive_data,omitempty"` } type SensitiveData struct { Filename string `json:"filename"` Path string `json:"path"` Type string `json:"type"` Hash string `json:"hash"` } type MalwareData struct { Malware string `json:"malware"` Path string `json:"path"` Hash string `json:"hash"` } type ImageAssuranceResults struct { Disallowed bool `json:"disallowed"` ChecksPerformed []ControlCheck `json:"checks_performed"` } type ControlCheck struct { Control string `json:"control"` PolicyName string `json:"policy_name"` Failed bool `json:"failed"` } type ScanOptions struct { ScanSensitiveData bool `json:"scan_sensitive_data"` ScanMalware bool `json:"scan_malware"` } type VulnerabilitySummary struct { Total int `json:"total"` Critical int `json:"critical"` High int `json:"high"` Medium int `json:"medium"` Low int `json:"low"` Negligible int `json:"negligible"` Sensitive int `json:"sensitive"` Malware int `json:"malware"` } type InfoResources struct { Vulnerabilities []Vulnerability `json:"vulnerabilities"` ResourceDetails `json:"resource"` } type ResourceDetails struct { Name string `json:"name"` Version string `json:"version"` } type Vulnerability struct { Name string `json:"name"` Version string `json:"version"` FixVersion string `json:"fix_version"` Severity string `json:"aqua_severity"` //`json:""`nvd_severity } ================================================ FILE: data/utils.go ================================================ package data import ( "regexp" ) func ClearField(source string) string { re := regexp.MustCompile(`[[:cntrl:]]|[\x{FFFD}]`) return re.ReplaceAllString(source, "") } ================================================ FILE: data/utils_test.go ================================================ package data import ( "testing" ) func TestClearField(t *testing.T) { tests := []struct { in, out string }{ {"test\r", "test"}, {"test\n", "test"}, {"the\xFF \xFDtest", "the test"}, } for _, test := range tests { if got := ClearField(test.in); got != test.out { t.Errorf("ClearField(%q) == %q, want %q", test.in, got, test.out) } } } ================================================ FILE: dbservice/actions.go ================================================ package dbservice import ( "time" bolt "go.etcd.io/bbolt" ) func MayBeStoreMessage(message []byte, messageKey string, expired *time.Time) (wasStored bool, err error) { mutex.Lock() defer mutex.Unlock() db, err := bolt.Open(DbPath, 0666, nil) if err != nil { return false, err } defer db.Close() if err = Init(db, dbBucketName); err != nil { return false, err } if err = Init(db, dbBucketExpiryDates); err != nil { return false, err } currentValue, err := dbSelect(db, dbBucketName, messageKey) if err != nil { return false, err } if currentValue != nil { return false, nil } else { bMessageKey := []byte(messageKey) err = dbInsert(db, dbBucketName, bMessageKey, message) if err != nil { return false, err } if expired != nil { err = dbInsert(db, dbBucketExpiryDates, []byte(expired.Format(DateFmt)), bMessageKey) if err != nil { return false, err } } return true, nil } } ================================================ FILE: dbservice/changedbpath_test.go ================================================ package dbservice import "testing" func TestChangeDbPath(t *testing.T) { testPath := "/tmp/test.db" storedPath := DbPath ChangeDbPath(testPath) defer func() { ChangeDbPath(storedPath) }() if DbPath != testPath { t.Errorf("path is not configured correctly, expected: %s, got %s", testPath, DbPath) } } ================================================ FILE: dbservice/checker.go ================================================ package dbservice import ( "bytes" "github.com/aquasecurity/postee/v2/utils" "log" "time" bolt "go.etcd.io/bbolt" ) func CheckSizeLimit() { if DbSizeLimit == 0 { return } mutex.Lock() defer mutex.Unlock() db, err := bolt.Open(DbPath, 0666, nil) if err != nil { log.Println("CheckSizeLimit: Can't open db:", DbPath) return } defer db.Close() if err := db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(dbBucketName)) if b == nil { return nil } c := b.Cursor() size := 0 for k, v := c.First(); k != nil; k, v = c.Next() { size += len(v) } if size > DbSizeLimit { err = tx.DeleteBucket([]byte(dbBucketName)) if err != nil { return err } err = tx.DeleteBucket([]byte(dbBucketExpiryDates)) if err != nil { return err } utils.Debug("DB size: %db is over size limit: %db, DB is cleared", size, DbSizeLimit) } return nil }); err != nil { log.Println("Error a check of db size:", err) return } } func CheckExpiredData() { mutex.Lock() defer mutex.Unlock() db, err := bolt.Open(DbPath, 0666, nil) if err != nil { log.Println("CheckExpiredData: Can't open db:", DbPath) return } defer db.Close() expired, err := getExpired(db) if err != nil { log.Println("Can't select expired data: ", err) return } if err := dbDelete(db, dbBucketName, expired); err != nil { log.Println("Can't remove expired data: ", err) } } func getExpired(db *bolt.DB) (keys [][]byte, err error) { keys = [][]byte{} ttlKeys := [][]byte{} if err = db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(dbBucketExpiryDates)) if b == nil { return nil } c := b.Cursor() max := []byte(time.Now().UTC().Format(DateFmt)) //remove expired records for k, v := c.First(); k != nil && bytes.Compare(k, max) <= 0; k, v = c.Next() { keys = append(keys, v) ttlKeys = append(ttlKeys, k) } return nil }); err != nil { return nil, err } if err = dbDelete(db, dbBucketExpiryDates, ttlKeys); err != nil { return nil, err } return } ================================================ FILE: dbservice/checker_test.go ================================================ package dbservice import ( "github.com/stretchr/testify/assert" "os" "testing" "time" bolt "go.etcd.io/bbolt" ) func TestExpiredDates(t *testing.T) { dbPathReal := DbPath realDueTimeBase := dueTimeBase defer func() { os.Remove(DbPath) DbPath = dbPathReal dueTimeBase = realDueTimeBase }() dueTimeBase = time.Nanosecond DbPath = "test_webhooks.db" tests := []struct { title string delay int uniqueMessageTimeoutSeconds int needRun bool wasStored bool }{ {"Add initial scan", 0, 1, false, true}, {"Add same scan again - not stored", 0, 0, true, false}, {"Add same scan again - after delay - stored", 1, 0, true, true}, } for _, test := range tests { t.Log(test.title) if test.needRun { time.Sleep(time.Duration(test.delay) * time.Second) CheckExpiredData() } timeToExpire := time.Duration(test.uniqueMessageTimeoutSeconds) * time.Second expired := time.Now().UTC().Add(timeToExpire) wasStored, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, &expired) if err != nil { t.Fatal("First Add AlpineImageResult Error", err) } if wasStored != test.wasStored { t.Errorf("Error handling! Want wasStored: %t, got: %t", test.wasStored, wasStored) } } } func TestCheckSizeLimit(t *testing.T) { dbPathReal := DbPath realSizeLimit := DbSizeLimit defer func() { DbPath = dbPathReal DbSizeLimit = realSizeLimit }() DbPath = "test_webhooks.db" tests := []struct { name string dbSizeLimit int wasCleared bool }{ { name: "DB has been cleared", dbSizeLimit: 1, wasCleared: true, }, { name: "DB not cleared", wasCleared: false, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { DbSizeLimit = test.dbSizeLimit db, err := bolt.Open(DbPath, 0666, nil) if err != nil { t.Fatal("Can't open db:", DbPath) } defer func() { os.Remove(DbPath) db.Close() }() err = dbInsert(db, dbBucketName, []byte("sha256:12345"), []byte("input_struct")) if err != nil { t.Fatal("TestDbDelete dbInsert: ", err) } err = dbInsert(db, dbBucketExpiryDates, []byte("2222-02-22T04:37:25.251356543Z"), []byte("sha256:12345")) if err != nil { t.Fatal("TestDbDelete dbInsert: ", err) } err = db.Close() // CheckSizeLimit() will open DB. We must close DB before doing this. if err != nil { t.Errorf("unable close DB: %v", err) } CheckSizeLimit() existDbBucketName, err := dbBucketExists(db, dbBucketName) if err != nil { t.Errorf("Unable to check if bucket exists: %v", err) } existDbBucketExpiryDates, err := dbBucketExists(db, dbBucketExpiryDates) if err != nil { t.Errorf("Unable to check if bucket exists: %v", err) } if test.wasCleared { assert.False(t, existDbBucketName) assert.False(t, existDbBucketExpiryDates) } else { assert.True(t, existDbBucketName) assert.True(t, existDbBucketExpiryDates) } }) } } func TestWrongBuckets(t *testing.T) { savedDbBucketName := dbBucketName savedDbBucketExpiryDates := dbBucketExpiryDates dbPathReal := DbPath defer func() { dbBucketName = savedDbBucketName dbBucketExpiryDates = savedDbBucketExpiryDates os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "test_webhooks.db" _, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil) if err != nil { t.Fatal(err) } DbSizeLimit = 1 dbBucketName = "" dbBucketExpiryDates = "" CheckSizeLimit() dbBucketName = "dbBucketName" _, err = MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil) if err == nil { t.Error("No error for empty dbBucketExpiryDates") } dbBucketExpiryDates = "dbBucketExpiryDates" dbBucketName = "" _, err = MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil) if err == nil { t.Error("No error for empty dbBucketName") } } func TestDbDelete(t *testing.T) { dbPathReal := DbPath defer func() { os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "test_webhooks.db" db, err := bolt.Open(DbPath, 0666, nil) if err != nil { t.Fatal("Can't open db:", DbPath) } defer db.Close() key := []byte("key") value := []byte("value") bucket := "b" err = dbInsert(db, bucket, key, value) if err != nil { t.Fatal("TestDbDelete dbInsert: ", err) } err = dbDelete(db, bucket, [][]byte{key}) if err != nil { t.Fatal("TestDbDelete dbInsert: ", err) } err = db.Close() if err != nil { t.Errorf("Unable close DB: %v", err) } exist, err := dbBucketExists(db, bucket) if err != nil { t.Errorf("Unable to check if bucket exists: %v", err) } if !exist { t.Errorf("bucket hasn't been removed ") } } func TestWithoutAccessToDb(t *testing.T) { dbPathReal := DbPath defer func() { os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "test_webhooks.db" db, err := bolt.Open(DbPath, 0220, nil) if err != nil { t.Fatal("Can't open db:", DbPath) } db.Close() DbSizeLimit = 1 CheckSizeLimit() CheckExpiredData() } func dbBucketExists(db *bolt.DB, bucket string) (bool, error) { bucketExist := false db, err := bolt.Open(DbPath, 0666, nil) if err != nil { return false, err } defer func() { db.Close() }() err = db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucket)) if b != nil { bucketExist = true return nil } return nil }) if err != nil { return false, err } return bucketExist, nil } ================================================ FILE: dbservice/dbaggregator.go ================================================ package dbservice import ( "encoding/json" bolt "go.etcd.io/bbolt" ) func AggregateScans(output string, currentScan map[string]string, scansPerTicket int, ignoreTheQuantity bool) ([]map[string]string, error) { mutex.Lock() defer mutex.Unlock() db, err := bolt.Open(DbPath, 0666, nil) if err != nil { return nil, err } defer db.Close() err = Init(db, dbBucketAggregator) if err != nil { return nil, err } aggregatedScans := make([]map[string]string, 0, scansPerTicket) if len(currentScan) > 0 { aggregatedScans = append(aggregatedScans, currentScan) } currentValue, err := dbSelect(db, dbBucketAggregator, output) if err != nil { return nil, err } if len(currentValue) > 0 { var savedScans []map[string]string err = json.Unmarshal(currentValue, &savedScans) if err != nil { return nil, err } aggregatedScans = append(aggregatedScans, savedScans...) } if ignoreTheQuantity || len(aggregatedScans) < scansPerTicket { saving, err := json.Marshal(aggregatedScans) if err != nil { return nil, err } err = dbInsert(db, dbBucketAggregator, []byte(output), saving) if err != nil { return nil, err } return nil, nil } err = dbInsert(db, dbBucketAggregator, []byte(output), nil) if err != nil { return nil, err } return aggregatedScans, nil } ================================================ FILE: dbservice/dbaggregator_test.go ================================================ package dbservice import ( "os" "testing" ) func TestAggregateScans(t *testing.T) { var ( scan1 = map[string]string{"title": "t1", "description": "d1"} scan2 = map[string]string{"title": "t2", "description": "d2"} scan3 = map[string]string{"title": "t3", "description": "d3"} scan4 = map[string]string{"title": "t4", "description": "d4"} ) var tests = [...]struct { output string currentScan map[string]string scansPerTicket int want []map[string]string }{ { "jira", scan1, 3, nil, }, { "jira", scan2, 3, nil, }, { "jira", scan3, 3, []map[string]string{scan3, scan2, scan1}, }, { "jira", scan4, 3, nil, }, } dbPathReal := DbPath defer func() { os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "test_webhooks.db" for i := 0; i < len(tests); i++ { test := tests[i] aggregated, err := AggregateScans(test.output, test.currentScan, test.scansPerTicket, false) if err != nil { t.Errorf("AggregateScans Error: %v", err) continue } if len(aggregated) != len(test.want) { t.Errorf("Wrong result size\nResult: %v\nWaited: %v", aggregated, test.want) continue } for i := 0; i < len(aggregated); i++ { if aggregated[i]["title"] != test.want[i]["title"] { t.Errorf("Wrong title\nResult: %q\nWaited: %q", aggregated[i]["title"], test.want[i]["title"]) } if aggregated[i]["description"] != test.want[i]["description"] { t.Errorf("Wrong Description\nResult: %q\nWaited: %q", aggregated[i]["description"], test.want[i]["description"]) } } } // Test of existence last scan in DB lastScan, err := AggregateScans("jira", nil, 0, false) if err != nil { t.Fatalf("AggregateScans Error: %v", err) } if len(lastScan) != 1 { t.Fatalf("Db don't contain last scan") } if lastScan[0]["title"] != scan4["title"] { t.Errorf("Wrong title\nResult: %q\nWaited: %q", lastScan[0]["title"], scan4["title"]) } if lastScan[0]["description"] != scan4["description"] { t.Errorf("Wrong Description\nResult: %q\nWaited: %q", lastScan[0]["description"], scan4["description"]) } } ================================================ FILE: dbservice/dbparam.go ================================================ package dbservice import ( "log" "os" "path/filepath" "sync" "time" ) var ( dbBucketName = "WebhookBucket" dbBucketAggregator = "WebhookAggregator" dbBucketExpiryDates = "WebookExpiryDates" DbBucketActionStats = "WebhookActionStats" DbBucketSharedConfig = "WebhookSharedConfig" DbSizeLimit = 0 dueTimeBase = time.Hour * time.Duration(24) DateFmt = time.RFC3339Nano DbPath = "/server/database/webhooks.db" mutex sync.Mutex ) func ChangeDbPath(newPath string) { mutex.Lock() DbPath = newPath mutex.Unlock() } func SetNewDbPathFromEnv() { newPath := os.Getenv("PATH_TO_DB") if newPath != "" { if _, err := os.Stat(newPath); err != nil { if os.IsNotExist(err) { err = os.MkdirAll(filepath.Dir(newPath), os.ModePerm) if err != nil { log.Printf("Can't create DateBase directory: %v, the default path is used", err) return } } else { log.Printf("Can't check DateBase directory: %v, the default path is used", err) return } } ChangeDbPath(newPath) } } ================================================ FILE: dbservice/dbparam_test.go ================================================ package dbservice import ( "os" "path/filepath" "strings" "testing" ) func TestSetNewDbPathFromEnv(t *testing.T) { envPathToDbOld := os.Getenv("PATH_TO_DB") defer os.Setenv("PATH_TO_DB", envPathToDbOld) dbPathOld := DbPath defaultDbPath := "/server/database/webhooks.db" var tests = []struct { name string envPathToDb string changePermission bool expectedDBPath string }{ {"Empty PATH_TO_DB", "", false, defaultDbPath}, {"Permission denied to create directory(default DbPath is used)", "/database/database.db", false, defaultDbPath}, {"New DbPath", "./base/base.db", false, "./base/base.db"}, {"Permission denied to check directory(default DbPath is used)", "webhook/database/webhooks.db", true, defaultDbPath}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { os.Setenv("PATH_TO_DB", test.envPathToDb) baseDir := strings.Split(filepath.Dir(test.envPathToDb), "/")[0] if test.changePermission { err := os.Mkdir(baseDir, os.ModeDir) if err != nil { t.Errorf("Can't create dir: %s", baseDir) } err = os.Chmod(baseDir, 0) if err != nil { t.Errorf("Can't change the mode dir in %s: %s", baseDir, err) } } SetNewDbPathFromEnv() defer os.RemoveAll(baseDir) defer ChangeDbPath(dbPathOld) if test.expectedDBPath != DbPath { t.Errorf("[%s] Paths is not equals, expected: %s, got: %s", test.name, test.expectedDBPath, DbPath) } }) } } ================================================ FILE: dbservice/dbservice_test.go ================================================ package dbservice import ( "errors" "os" "testing" "time" "go.etcd.io/bbolt" ) var ( AlpineImageKey = "sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1-alpine:3.8-Docker Hub" AlpineImageResult = `{ "image": "alpine:3.8", "registry": "Docker Hub", "digest": "sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1", "previous_digest": "sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1", "image_assurance_results": { "disallowed": true, "checks_performed": [ { "control": "max_severity", "policy_name": "Default", "failed": false }, { "control": "trusted_base_images", "policy_name": "Default", "failed": true }, { "control": "max_score", "policy_name": "Default", "failed": false } ] }, "vulnerability_summary": { "total": 2, "critical": 0, "high": 0, "medium": 2, "low": 0, "negligible": 0, "sensitive": 0, "malware": 0 }, "scan_options": { "scan_sensitive_data": true, "scan_malware": true }, "resources": [ { "vulnerabilities": [ { "name": "CVE-2018-20679", "version": "", "fix_version": "", "aqua_severity": "medium" }, { "name": "CVE-2019-5747", "version": "", "fix_version": "", "aqua_severity": "medium" } ], "resource": { "name": "busybox", "version": "1.28.4-r3" } } ] }` ) func TestStoreMessage(t *testing.T) { var tests = []struct { input *string }{ {&AlpineImageResult}, } dbPathReal := DbPath defer func() { os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "test_webhooks.db" for _, test := range tests { // Handling of first scan isNew, err := MayBeStoreMessage([]byte(*test.input), AlpineImageKey, nil) if err != nil { t.Errorf("Error: %s\n", err) } if !isNew { t.Errorf("A first scan was found!\n") } // Handling of second scan with the same data isNew, err = MayBeStoreMessage([]byte(*test.input), AlpineImageKey, nil) if err != nil { t.Errorf("Error: %s\n", err) } if isNew { t.Errorf("A old scan wasn't found!\n") } } } func TestInitError(t *testing.T) { originalInit := Init originalDbPath := DbPath initErr := errors.New("init error") DbPath = "test_webhooks.db" Init = func(db *bbolt.DB, bucket string) error { return initErr } defer func() { Init = originalInit os.Remove(DbPath) DbPath = originalDbPath }() isNew, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil) if isNew { t.Errorf("Scan shouldn't be marked as new\n") } if !errors.Is(err, initErr) { t.Errorf("Unexpected error: expected %s, got %s \n", initErr, err) } } func TestSelectError(t *testing.T) { originalDbSelect := dbSelect originalDbPath := DbPath selectErr := errors.New("select error") DbPath = "test_webhooks.db" dbSelect = func(db *bbolt.DB, bucket, key string) (result []byte, err error) { return nil, selectErr } defer func() { dbSelect = originalDbSelect os.Remove(DbPath) DbPath = originalDbPath }() isNew, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, nil) if isNew { t.Errorf("Scan shouldn't be marked as new\n") } if !errors.Is(err, selectErr) { t.Errorf("Unexpected error: expected %s, got %s \n", selectErr, err) } } func TestInsertError(t *testing.T) { var tests = []struct { bucket string }{ {"WebhookBucket"}, {"WebookExpiryDates"}, } for _, test := range tests { testBucketInsert(t, test.bucket) } } func testBucketInsert(t *testing.T, testBucket string) { originalDbInsert := dbInsert originalDbPath := DbPath insertErr := errors.New("insert error") DbPath = "test_webhooks.db" dbInsert = func(db *bbolt.DB, bucket string, key, value []byte) error { if bucket == testBucket { return insertErr } return nil } defer func() { dbInsert = originalDbInsert os.Remove(DbPath) DbPath = originalDbPath }() //expired shouldn't be null to cause insert to 'WebookExpiryDates' bucket timeToExpire := time.Duration(1) * time.Second expired := time.Now().UTC().Add(timeToExpire) isNew, err := MayBeStoreMessage([]byte(AlpineImageResult), AlpineImageKey, &expired) if isNew { t.Errorf("Scan shouldn't be marked as new\n") } if !errors.Is(err, insertErr) { t.Errorf("Unexpected error: expected %s, got %s \n", insertErr, err) } } ================================================ FILE: dbservice/delete.go ================================================ package dbservice import bolt "go.etcd.io/bbolt" func dbDelete(db *bolt.DB, bucket string, keys [][]byte) error { return db.Update(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucket)) if b == nil { return nil } for _, key := range keys { if err := b.Delete(key); err != nil { return err } } return nil }) } ================================================ FILE: dbservice/init.go ================================================ package dbservice import "go.etcd.io/bbolt" var Init = func(db *bbolt.DB, bucket string) error { return db.Update(func(tx *bbolt.Tx) error { _, err := tx.CreateBucketIfNotExists([]byte(bucket)) return err }) } ================================================ FILE: dbservice/insert.go ================================================ package dbservice import bolt "go.etcd.io/bbolt" var dbInsert = func(db *bolt.DB, bucket string, key, value []byte) error { err := db.Update(func(tx *bolt.Tx) error { b, err := tx.CreateBucketIfNotExists([]byte(bucket)) if err != nil { return err } return b.Put(key, value) }) return err } ================================================ FILE: dbservice/invalidinit_test.go ================================================ package dbservice import ( "errors" "os" "testing" "go.etcd.io/bbolt" ) var tests = []struct { caseDesc string errPrvdr func() error initIsNotCalled bool }{ { caseDesc: "EnsureApiKey", errPrvdr: func() error { return EnsureApiKey() }, }, { caseDesc: "GetApiKey", errPrvdr: func() error { _, err := GetApiKey() return err }, initIsNotCalled: true, }, { caseDesc: "RegisterPlgnInvctn", errPrvdr: func() error { return RegisterPlgnInvctn("some-key") }, }, { caseDesc: "MayBeStoreMessage", errPrvdr: func() error { _, err := MayBeStoreMessage(nil, "a-b-c", nil) return err }, }, { caseDesc: "AggregateScans", errPrvdr: func() error { _, err := AggregateScans("", map[string]string{}, 1, false) return err }, }, } func TestInvalidDbPath(t *testing.T) { dbPathReal := DbPath defer func() { os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "/tmp" for _, test := range tests { err := test.errPrvdr() if err == nil { t.Errorf("Error is expected when %s is called\n", test.caseDesc) } } } func TestBucketInitialization(t *testing.T) { savedInit := Init dbPathReal := DbPath defer func() { os.Remove(DbPath) Init = savedInit DbPath = dbPathReal }() DbPath = "test_webhooks.db" expectedError := errors.New("weird error") Init = func(db *bbolt.DB, bucket string) error { return expectedError } for _, test := range tests { if test.initIsNotCalled { continue } err := test.errPrvdr() if !errors.Is(err, expectedError) { t.Errorf("Unexpected error for %s call, expected %v, got %v", test.caseDesc, expectedError, err) } } } ================================================ FILE: dbservice/plgnstats.go ================================================ package dbservice import ( "strconv" bolt "go.etcd.io/bbolt" ) func RegisterPlgnInvctn(name string) error { mutex.Lock() defer mutex.Unlock() db, err := bolt.Open(DbPath, 0666, nil) if err != nil { return err } defer db.Close() err = Init(db, DbBucketActionStats) if err != nil { return err } err = db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(DbBucketActionStats)) var i int v := bucket.Get([]byte(name)) if v == nil { i = 0 } else { i, err = strconv.Atoi(string(v[:])) } i++ nwv := strconv.Itoa(i) err = bucket.Put([]byte(name), []byte(nwv)) return err }) return err } ================================================ FILE: dbservice/plgnstats_test.go ================================================ package dbservice import ( "os" "strconv" "testing" bolt "go.etcd.io/bbolt" ) func TestRegisterPlgnInvctn(t *testing.T) { dbPathReal := DbPath defer func() { os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "test_webhooks.db" expectedCnt := 3 keyToTest := "test" for i := 0; i < expectedCnt; i++ { err := RegisterPlgnInvctn(keyToTest) if err != nil { t.Fatal(err) } } r, err := getPlgnStats() if err != nil { t.Fatal("error while getting value of API key") } if r[keyToTest] != expectedCnt { t.Errorf("Persisted count doesn't match expected. Expected %d, got %d\n", r[keyToTest], expectedCnt) } } func getPlgnStats() (r map[string]int, err error) { r = make(map[string]int) db, err := bolt.Open(DbPath, 0444, nil) if err != nil { return nil, err } defer db.Close() err = db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(DbBucketActionStats)) if bucket == nil { return nil //no bucket - empty stats will be returned } c := bucket.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { cnt, err := strconv.Atoi(string(v[:])) if err != nil { return err } r[string(k[:])] = cnt } return nil }) if err != nil { return nil, err } return r, nil } ================================================ FILE: dbservice/select.go ================================================ package dbservice import ( bolt "go.etcd.io/bbolt" ) var dbSelect = func(db *bolt.DB, bucket, key string) (result []byte, err error) { err = db.View(func(tx *bolt.Tx) error { b := tx.Bucket([]byte(bucket)) r := b.Get([]byte(key)) if r != nil { result = make([]byte, len(r)) copy(result, r) } return nil }) return } ================================================ FILE: dbservice/sharedcfg.go ================================================ package dbservice import ( "crypto/rand" "encoding/hex" "errors" "io" "os" bolt "go.etcd.io/bbolt" ) const ( apiKeyName = "POSTEE_API_KEY" ) func getDbPath() string { var dbPath string if len(os.Getenv("PATH_TO_DB")) > 0 { dbPath = os.Getenv("PATH_TO_DB") } else { dbPath = DbPath } return dbPath } func EnsureApiKey() error { mutex.Lock() defer mutex.Unlock() db, err := bolt.Open(getDbPath(), 0666, nil) if err != nil { return err } defer db.Close() err = Init(db, DbBucketActionStats) if err != nil { return err } newApiKey, err := generateApiKey(32) if err != nil { return err } err = dbInsert(db, DbBucketSharedConfig, []byte(apiKeyName), []byte(newApiKey)) return err } func GetApiKey() (string, error) { var apiKey string = "" db, err := bolt.Open(getDbPath(), 0444, nil) //should be enough if err != nil { return "", err } defer db.Close() err = db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(DbBucketSharedConfig)) if bucket == nil { return errors.New("no bucket") //no bucket } bytes := bucket.Get([]byte(apiKeyName)) apiKey = string(bytes[:]) return nil }) if err != nil { return "", err } return apiKey, nil } func generateApiKey(length int) (string, error) { k := make([]byte, length) if _, err := io.ReadFull(rand.Reader, k); err != nil { return "", err } return hex.EncodeToString(k), nil } ================================================ FILE: dbservice/sharedcfg_test.go ================================================ package dbservice import ( "os" "testing" ) func TestApiKey(t *testing.T) { dbPathReal := DbPath defer func() { os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "test_webhooks.db" err := EnsureApiKey() if err != nil { t.Fatal("error EnsureApiKey") } key, err := GetApiKey() if err != nil { t.Fatal("error while getting value of API key") } if key == "" { t.Fatal("empty key received") } } func TestApiKeyWithoutInit(t *testing.T) { dbPathReal := DbPath defer func() { os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "test_webhooks.db" key, err := GetApiKey() if err == nil { t.Fatal("Error is expected") } if key != "" { t.Fatal("Empty key is expected") } } func TestApiKeyRenewal(t *testing.T) { dbPathReal := DbPath defer func() { os.Remove(DbPath) DbPath = dbPathReal }() DbPath = "test_webhooks.db" var keys [2]string for i := 0; i < 2; i++ { err := EnsureApiKey() if err != nil { t.Errorf("error EnsureApiKey: %s", err) } key, err := GetApiKey() if err != nil { t.Fatal("error while getting value of API key") } if key == "" { t.Fatal("empty key received") } keys[i] = key } if keys[0] == keys[1] { t.Errorf("Key is not updated. (before: %s and after update: %s)", keys[0], keys[1]) } } ================================================ FILE: deploy/helm/postee/.helmignore ================================================ # Patterns to ignore when building packages. # This supports shell glob matching, relative path matching, and # negation (prefixed with !). Only one pattern per line. .DS_Store # Common VCS dirs .git/ .gitignore .bzr/ .bzrignore .hg/ .hgignore .svn/ # Common backup files *.swp *.bak *.tmp *.orig *~ # Various IDEs .project .idea/ *.tmproj .vscode/ ================================================ FILE: deploy/helm/postee/Chart.yaml ================================================ apiVersion: v2 name: postee description: A Helm chart for Postee type: application # Versions are expected to follow Semantic Versioning (https://semver.org/) version: 0.5.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. appVersion: "2.14.0-amd64" keywords: - aquasecurity - postee sources: - https://github.com/aquasecurity/postee ================================================ FILE: deploy/helm/postee/templates/NOTES.txt ================================================ 1. Get the application URL by running these commands: {{- if .Values.ingress.enabled }} {{- range $host := .Values.ingress.hosts }} {{- range .paths }} http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "postee.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "postee.fullname" . }}' export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "postee.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} 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}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Postee Webhook http://{{ include "postee.fullname" . }}:{{ .Values.service.port }} to set in your application." echo "Postee Website http://{{ include "postee.ui.fullname" . }}:{{ .Values.uiService.port }} to configure Postee." kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME {{ .Values.uiService.port }}:$CONTAINER_PORT {{- end }} ================================================ FILE: deploy/helm/postee/templates/_helpers.tpl ================================================ {{/* Expand the name of the chart. */}} {{- define "postee.name" -}} {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Create a default fully qualified app name. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). If release name contains chart name it will be used as a full name. */}} {{- define "postee.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }} {{- else }} {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{- define "postee.ui.fullname" -}} {{- if .Values.fullnameOverride }} {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}ui {{- else }} {{- $name := default .Chart.Name .Values.nameOverride }} {{- if contains $name .Release.Name }} {{- .Release.Name | trunc 63 | trimSuffix "-" }}ui {{- else }} {{- printf "%s-%sui" .Release.Name $name | trunc 63 | trimSuffix "-" }} {{- end }} {{- end }} {{- end }} {{/* Create chart name and version as used by the chart label. */}} {{- define "postee.chart" -}} {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} {{- end }} {{/* Common labels */}} {{- define "postee.labels" -}} helm.sh/chart: {{ include "postee.chart" . }} {{ include "postee.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{- define "postee.ui.labels" -}} helm.sh/chart: {{ include "postee.chart" . }} {{ include "postee.ui.selectorLabels" . }} {{- if .Chart.AppVersion }} app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} {{- end }} app.kubernetes.io/managed-by: {{ .Release.Service }} {{- end }} {{/* Selector labels */}} {{- define "postee.selectorLabels" -}} app.kubernetes.io/name: {{ include "postee.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{- define "postee.ui.selectorLabels" -}} app.kubernetes.io/name: {{ include "postee.name" . }}-ui app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} {{/* Create the name of the service account to use */}} {{- define "postee.serviceAccountName" -}} {{- if .Values.serviceAccount.create }} {{- default (include "postee.fullname" .) .Values.serviceAccount.name }} {{- else }} {{- default "default" .Values.serviceAccount.name }} {{- end }} {{- end }} ================================================ FILE: deploy/helm/postee/templates/cfg-secret.yaml ================================================ {{ if not .Values.configuration.existingSecret.enabled }} apiVersion: v1 kind: Secret metadata: name: {{ include "postee.fullname" . }}-secret data: cfg.yaml: | {{ .Values.posteeConfig | b64enc | indent 4 }} {{- end }} ================================================ FILE: deploy/helm/postee/templates/ingress.yaml ================================================ {{- if .Values.ingress.enabled -}} {{- $fullName := include "postee.fullname" . -}} {{- $svcPort := .Values.service.port -}} {{- if semverCompare ">=1.14-0 < 1.19.0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1beta1 {{- else if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} apiVersion: networking.k8s.io/v1 {{- else -}} apiVersion: extensions/v1beta1 {{- end }} kind: Ingress metadata: name: {{ $fullName }} labels: {{- include "postee.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} spec: {{- if .Values.ingress.tls }} tls: {{- range .Values.ingress.tls }} - hosts: {{- range .hosts }} - {{ . | quote }} {{- end }} secretName: {{ .secretName }} {{- end }} {{- end }} rules: {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} {{- range .Values.ingress.hosts }} - host: {{ .host }} http: paths: {{- range .paths }} - path: {{ .path }} pathType: {{ .pathType }} backend: service: name: {{ $fullName }} port: number: {{ $svcPort }} {{- end }} {{- end }} {{- else -}} {{- range .Values.ingress.hosts }} - host: {{ .host | quote }} http: paths: {{- range .paths }} - path: {{ .path }} backend: serviceName: {{ $fullName }} servicePort: {{ $svcPort }} {{- end }} {{- end }} {{- end }} {{- end }} ================================================ FILE: deploy/helm/postee/templates/postee-svc.yaml ================================================ apiVersion: v1 kind: Service metadata: name: {{ include "postee.fullname" . }} labels: {{- include "postee.labels" . | nindent 4 }} spec: type: {{ .Values.service.type }} ports: - port: {{ .Values.service.port }} targetPort: {{ .Values.service.targetPort }} protocol: TCP name: http - port: {{ .Values.service.portSsl }} targetPort: {{ .Values.service.targetPortSsl }} protocol: TCP name: http-ssl selector: {{- include "postee.selectorLabels" . | nindent 4 }} ================================================ FILE: deploy/helm/postee/templates/postee-ui-secret.yaml ================================================ {{- if not .Values.posteUi.existingSecret.enabled }} apiVersion: v1 kind: Secret metadata: name: {{ include "postee.ui.fullname" . }}-creds labels: {{- include "postee.ui.labels" . | nindent 4 }} type: Opaque data: postee-ui-user: {{ .Values.posteUi.user | b64enc | quote }} postee-ui-password: {{ .Values.posteUi.pass | b64enc | quote }} {{- end }} ================================================ FILE: deploy/helm/postee/templates/postee-ui-svc.yaml ================================================ apiVersion: v1 kind: Service metadata: name: {{ include "postee.ui.fullname" . }} labels: {{- include "postee.ui.labels" . | nindent 4 }} spec: type: {{ .Values.uiService.type }} ports: - port: {{ .Values.uiService.port }} targetPort: {{ .Values.uiService.targetPort }} protocol: TCP name: http selector: {{- include "postee.ui.selectorLabels" . | nindent 4 }} ================================================ FILE: deploy/helm/postee/templates/postee-ui.yaml ================================================ {{- $fullName := include "postee.fullname" . -}} apiVersion: apps/v1 kind: Deployment metadata: name: {{ include "postee.ui.fullname" . }} labels: {{- include "postee.ui.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "postee.ui.selectorLabels" . | nindent 6 }} template: metadata: {{- with .Values.podAnnotations }} annotations: {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "postee.ui.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "postee.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} initContainers: - name: setting-cfg image: "{{ .Values.imageInit.repository }}:{{ .Values.imageInit.tag }}" imagePullPolicy: {{ .Values.imageInit.pullPolicy }} command: ["/bin/chown", "-R", "1099", "{{ .Values.persistentVolume.mountPathConfig }}"] volumeMounts: - name: postee-config mountPath: {{ .Values.persistentVolume.mountPathConfig }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.posteUi.image }}:{{ .Values.posteUi.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} env: - name: POSTEE_UI_CFG value: {{ .Values.persistentVolume.mountPathConfig }}/cfg.yaml - name: POSTEE_UI_PORT value: {{ .Values.posteUi.port | quote }} - name: POSTEE_UI_UPDATE_URL value: "http://{{ include "postee.fullname" . }}:{{ .Values.service.port }}" - name: POSTEE_ADMIN_USER valueFrom: secretKeyRef: name: {{ .Values.posteUi.existingSecret.secretName | default (printf "%s-creds" (include "postee.ui.fullname" .)) }} key: {{ .Values.posteUi.existingSecret.usernameKey | default "postee-ui-user" }} - name: POSTEE_ADMIN_PASSWORD valueFrom: secretKeyRef: name: {{ .Values.posteUi.existingSecret.secretName | default (printf "%s-creds" (include "postee.ui.fullname" .)) }} key: {{ .Values.posteUi.existingSecret.passwordKey | default "postee-ui-password" }} ports: - name: http containerPort: {{ .Values.posteUi.port }} protocol: TCP volumeMounts: - name: postee-config mountPath: {{ .Values.persistentVolume.mountPathConfig }} - name: postee-db mountPath: {{ .Values.persistentVolume.mountPathDb }} resources: {{- toYaml .Values.resources | nindent 12 }} volumes: - name: postee-db persistentVolumeClaim: claimName: "{{ $fullName }}-db-{{ $fullName }}-0" - name: postee-config persistentVolumeClaim: claimName: "{{ $fullName }}-config-{{ $fullName }}-0" {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} ================================================ FILE: deploy/helm/postee/templates/postee.yaml ================================================ {{- $fullName := include "postee.fullname" . -}} apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ include "postee.fullname" . }} labels: {{- include "postee.labels" . | nindent 4 }} spec: {{- if not .Values.autoscaling.enabled }} replicas: {{ .Values.replicaCount }} {{- end }} selector: matchLabels: {{- include "postee.selectorLabels" . | nindent 6 }} serviceName: {{ include "postee.fullname" . }} volumeClaimTemplates: - metadata: name: {{ $fullName }}-db spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi - metadata: name: {{ $fullName }}-config spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: {{ $fullName }}-rego-template spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: {{ $fullName }}-filters spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi template: metadata: annotations: {{- if not .Values.configuration.existingSecret.enabled }} checksum/secret: {{ include (print $.Template.BasePath "/cfg-secret.yaml") . | sha256sum }} {{- end }} {{- with .Values.podAnnotations }} {{- toYaml . | nindent 8 }} {{- end }} labels: {{- include "postee.selectorLabels" . | nindent 8 }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} {{- end }} serviceAccountName: {{ include "postee.serviceAccountName" . }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 8 }} initContainers: - name: setting-db image: "{{ .Values.imageInit.repository }}:{{ .Values.imageInit.tag }}" imagePullPolicy: {{ .Values.imageInit.pullPolicy }} command: ["/bin/chown", "-R", "1099", "{{ .Values.persistentVolume.mountPathDb }}"] volumeMounts: - name: {{ $fullName }}-db mountPath: {{ .Values.persistentVolume.mountPathDb }} - name: setting-cfg image: "{{ .Values.imageInit.repository }}:{{ .Values.imageInit.tag }}" imagePullPolicy: {{ .Values.imageInit.pullPolicy }} command: ["/bin/cp", "/k8s/cfg.yaml", "/data/cfg.yaml"] volumeMounts: - name: {{ $fullName }}-secret-vol mountPath: /k8s - name: {{ $fullName }}-config mountPath: {{ .Values.persistentVolume.mountPathConfig }} containers: - name: {{ .Chart.Name }} securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.image.pullPolicy }} env: - name: POSTEE_CFG value: /data/cfg.yaml - name: POSTEE_DEBUG value: "not" {{- with .Values.envFrom }} envFrom: {{- range . }} - secretRef: name: {{ . }} {{- end }} {{- end }} ports: - name: http containerPort: {{ .Values.service.targetPort }} protocol: TCP - name: tls containerPort: {{ .Values.service.targetPortSsl }} protocol: TCP volumeMounts: - name: {{ $fullName }}-db mountPath: {{ .Values.persistentVolume.mountPathDb }} - name: {{ $fullName }}-config mountPath: {{ .Values.persistentVolume.mountPathConfig }} - name: {{ $fullName }}-rego-template mountPath: {{ .Values.persistentVolume.mountPathRego }} - name: {{ $fullName }}-filters mountPath: {{ .Values.persistentVolume.mountPathFilters }} {{- if .Values.extraVolumeMounts }} {{ toYaml .Values.extraVolumeMounts | indent 12 }} {{- end }} resources: {{- toYaml .Values.resources | nindent 12 }} volumes: - secret: secretName: {{ $fullName }}-secret name: {{ $fullName }}-secret-vol {{- if .Values.extraVolumes }} {{ toYaml .Values.extraVolumes | indent 8 }} {{- end }} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} {{- end }} {{- with .Values.tolerations }} tolerations: {{- toYaml . | nindent 8 }} {{- end }} ================================================ FILE: deploy/helm/postee/templates/pvc.yaml ================================================ {{- if and .Values.persistentVolume.enabled (not .Values.persistentVolume.existingClaim) }} {{- $fullName := include "postee.fullname" . -}} kind: PersistentVolumeClaim apiVersion: v1 metadata: labels: {{- include "postee.labels" . | nindent 4 }} name: {{ include "postee.fullname" . }}-pvc spec: accessModes: {{- range .Values.persistentVolume.accessModes }} - {{ . | quote }} {{- end }} resources: requests: storage: {{ .Values.persistentVolume.size | quote }} {{- end }} ================================================ FILE: deploy/helm/postee/templates/serviceaccount.yaml ================================================ {{- if .Values.serviceAccount.create -}} apiVersion: v1 kind: ServiceAccount metadata: name: {{ include "postee.serviceAccountName" . }} labels: {{- include "postee.labels" . | nindent 4 }} {{- with .Values.serviceAccount.annotations }} annotations: {{- toYaml . | nindent 4 }} {{- end }} {{- end }} ================================================ FILE: deploy/helm/postee/templates/tests/test-connection.yaml ================================================ apiVersion: v1 kind: Pod metadata: name: "{{ include "postee.fullname" . }}-test-connection" labels: {{- include "postee.labels" . | nindent 4 }} annotations: "helm.sh/hook": test spec: containers: - name: wget image: busybox command: ['wget'] args: ['{{ include "postee.fullname" . }}:{{ .Values.service.Port }}'] restartPolicy: Never ================================================ FILE: deploy/helm/postee/values.yaml ================================================ # Default values for postee. # This is a YAML-formatted file. # Declare variables to be passed into your templates. replicaCount: 1 posteeConfig: | # The configuration file contains a general settings section, # routes, templates and actions sections. name: tenant # The tenant name aqua-server: # URL of Aqua Server for links. E.g. https://myserver.aquasec.com max-db-size: 1000MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited db-verify-interval: 1 # How often to check the DB size. By default, Postee checks every 1 hour # Routes are used to define how to handle an incoming message routes: - name: stdout actions: [ stdout ] template: raw-json #- name: route1 # Route name. Must be unique # input: contains(input.image, "alpine") # REGO rule to match input message against route # input-files: # Array filePaths to files with REGO rules # - Allow-Image-Name.rego # - Ignore-Image-Name.rego # - Allow-Registry.rego # - Ignore-Registry.rego # - Policy-Only-Fix-Available.rego # - Policy-Min-Vulnerability.rego # - Policy-Related-Features.rego # actions: [my-slack] # Action name (needs to be defined under "actions") which will receive the message # template: slack-template # Template name (needs to be defined under "templates") which will be used to process the message output format # plugins: # Optional plugins # aggregate-message-number: # Number of same messages to aggregate into one output message # 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 # 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 # 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 # - name: Trivy Operator Alerts # input: input.report.summary.criticalCount > 0 # You can customize this based on your needs # actions: [my-slack] # template: trivy-operator-slack # - name: Trivy Operator Sbom Report to Dependency Track # input: contains(input.kind, "SbomReport") # actions: [ my-dependencytrack ] # template: trivy-operator-dependencytrack # Templates are used to format a message templates: - name: vuls-slack # Out of the box template for slack rego-package: postee.vuls.slack # Slack template REGO package (available out of the box) - name: vuls-html # Out of the box HTML template rego-package: postee.vuls.html # HTML template REGO package (available out of the box) - name: raw-html # Raw message json rego-package: postee.rawmessage.html # HTML template REGO package (available out of the box) - name: legacy # Out of the box legacy Golang template legacy-scan-renderer: html - name: legacy-slack # Legacy slack template implemented in Golang legacy-scan-renderer: slack - name: legacy-jira # Legacy jira template implemented in Golang legacy-scan-renderer: jira - name: custom-email # Example of how to use a template from a Web URL url: # URL to custom REGO file - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json - name: vuls-cyclonedx # export vulnerabilities to CycloneDX XML rego-package: postee.vuls.cyclondx - name: trivy-operator-jira rego-package: postee.trivyoperator.jira - name: trivy-operator-slack rego-package: postee.trivyoperator.slack - name: trivy-operator-dependencytrack rego-package: postee.trivyoperator.dependencytrack # Rules are predefined rego policies that can be used to trigger routes rules: - name: Initial Access - name: Credential Access - name: Privilege Escalation - name: Defense Evasion - name: Persistence # Actions are target services that should consume the messages actions: - name: stdout type: stdout enable: true - name: my-jira # name must be unique type: jira # supported types: jira, email enable: false url: # Mandatory. E.g "https://johndoe.atlassian.net" user: # Mandatory. E.g :johndoe@gmail.com" password: # Optional. Specify Jira user API key. Used only for Jira Cloud token: # Optional. Specify Jira user Personal Access Token. Used only for Jira Server/Data Center project-key: # Mandatory. Specify the JIRA product key tls-verify: false board: # Optional. Specify the Jira board name to open tickets on labels: # Optional, specify array of labels to add to Ticket, for example: ["label1", "label2"] issuetype: # Optional. Specifty the issue type to open (Bug, Task, etc.). Default is "Task" priority: # Optional. Specify the issues severity. Default is "High" assignee: # Optional. Specify the assigned user. Default is the user that opened the ticket - name: my-email type: email enable: false user: # Optional (if auth supported): SMTP user name (e.g. johndoe@gmail.com) password: # Optional (if auth supported): SMTP password host: # Mandatory: SMTP host name (e.g. smtp.gmail.com) port: # Mandatory: SMTP server port (e.g. 587) sender: # Mandatory: The email address to use as a sender recipients: ["", ""] # Mandatory: comma separated list of recipients - name: my-email-smtp-server type: email enable: false use-mx: true sender: # Mandatory: The email address to use as a sender recipients: ["", ""] # Mandatory: comma separated list of recipients - name: my-slack type: slack enable: false url: https://hooks.slack.com/services/TAAAA/BBB/ - name: ms-team type: teams enable: false url: https://outlook.office.com/webhook/.... # Webhook's url - name: webhook type: webhook enable: false url: https://..../webhook/ # Webhook's url timeout: # Webhook's timeout. pattern is used, such as "300ms" or "2h45m". Default: 120s - name: splunk type: splunk enable: false url: http://localhost:8088 # Mandatory. Url of a Splunk server token: # Mandatory. a HTTP Event Collector Token size-limit: 10000 # Optional. Maximum scan length, in bytes. Default: 10000 tls-verify: false # Enable skip TLS Verification. Default: false. - name: my-servicenow type: serviceNow enable: false user: # Mandatory. E.g :johndoe@gmail.com" password: # Mandatory. Specify user API key instance: # Mandatory. Name of ServiceN ow Instance board: # Specify the ServiceNow board name to open tickets on. Default is "incident" - name: my-nexus-iq type: nexusIq enable: false user: # Mandatory. User name password: # Mandatory. User password url: # Mandatory. Url of Nexus IQ server organization-id: # Mandatory. Organization UID like "222de33e8005408a844c12eab952c9b0" - name: my-dependencytrack type: dependencytrack enable: false url: # Mandatory. Url of Dependency Track dependency-track-api-key: # Mandatory. API key of Dependency Track - name: my-opsgenie type: opsgenie enable: false token: # Mandatory. an API key from an API integration user: # Optional. Display name of the request owner. assignee: # Optional. Comma separated list of users that the alert will be routed to send notifications recipients: [""] # Optional. Comma separated list of users that the alert will become visible to without sending any notification tags: # Optional. Comma separated list of the alert tags. priority: # Optional. Specify the alert priority. Default is "P3" alias: # Optional. Client-defined identifier of the alert. entity: # Optional. Entity field of the alert that is generally used to specify which domain alert is related to. posteUi: port: 8000 user: "postee" pass: "changeme" ## Use an existing secret existingSecret: enabled: false # secretName: nameofsecret # usernameKey: username # passwordKey: password image: aquasec/postee-ui # By default `tag` is taken from `.Chart.AppVersion` # To use different version - uncomment this line and enter the desired version # tag: "" configuration: # If set to true, ensure the externally generated secret to be named # postee-secret and that it contains the JSON under a key called "cfg.yaml" existingSecret: enabled: false image: repository: aquasec/postee pullPolicy: Always # By default `tag` is taken from `.Chart.AppVersion` # To use different version - uncomment this line and enter the desired version # tag: "" imageInit: repository: busybox pullPolicy: IfNotPresent tag: "1.34" imagePullSecrets: [] nameOverride: "" fullnameOverride: "" serviceAccount: # Specifies whether a service account should be created create: true # Annotations to add to the service account annotations: {} name: "" podAnnotations: {} podSecurityContext: {} # fsGroup: 2000 securityContext: {} # capabilities: # drop: # - ALL # readOnlyRootFilesystem: true # runAsNonRoot: true # runAsUser: 1000 service: type: ClusterIP portSsl: 8445 targetPortSsl: 8445 port: 8082 targetPort: 8082 uiService: type: LoadBalancer port: 8000 targetPort: 8000 ingress: enabled: false annotations: {} # kubernetes.io/ingress.class: nginx # kubernetes.io/tls-acme: "true" hosts: - host: chart-example.local paths: - path: / pathType: Prefix backend: serviceName: chart-example.local servicePort: 80 tls: [] resources: limits: cpu: 500m memory: 256Mi requests: cpu: 200m memory: 128Mi autoscaling: enabled: false minReplicas: 1 maxReplicas: 100 targetCPUUtilizationPercentage: 80 # targetMemoryUtilizationPercentage: 80 nodeSelector: {} tolerations: [] affinity: {} ## Enable persistence using Persistent Volume Claims ## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ ## persistentVolume: enabled: false mountPathDb: /server/database mountPathConfig: /data mountPathRego: /server/rego-templates/custom mountPathFilters: /server/rego-filters/custom accessModes: - ReadWriteOnce size: 1Gi annotations: {} ## Persistent Volume Storage Class ## If defined, storageClassName: ## If set to "-", storageClassName: "", which disables dynamic provisioning ## If undefined (the default) or set to null, no storageClassName spec is ## set, choosing the default provisioner. (gp2 on AWS, standard on ## GKE, AWS & OpenStack) ## # storageClass: "-" # existingClaim: "" ## Secrets as environment variables ## If defined, these secrets are mounted as environment variables ## envFrom: ## - my-env-secret-1 # Extra Volumes to add to the postee Statefulset extraVolumes: [] # - name: example_exec_env # secret: # defaultMode: 420 # secretName: example_exec_script # Extra Volumes Mounts to add to the postee Statefulset extraVolumeMounts: [] # - name: example_exec_env # mountPath: /actions/exec/example_exec_env # subPath: example_exec_env # readOnly: true ================================================ FILE: deploy/kubernetes/hostPath/postee-pv.yaml ================================================ #Create the volume for the Postee volumeClaimTemplates --- apiVersion: v1 kind: PersistentVolume metadata: name: postee-db labels: app: postee-db spec: capacity: storage: 1Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain hostPath: path: /tmp/postee/db/ --- apiVersion: v1 kind: PersistentVolume metadata: name: postee-config labels: app: postee-config spec: capacity: storage: 100Mi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain hostPath: path: /tmp/postee/config/ --- apiVersion: v1 kind: PersistentVolume metadata: name: postee-rego-templates labels: app: postee-rego-templates spec: capacity: storage: 100Mi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain hostPath: path: /tmp/postee/rego-templates/ --- apiVersion: v1 kind: PersistentVolume metadata: name: postee-rego-filters labels: app: postee-rego-filters spec: capacity: storage: 100Mi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Retain hostPath: path: /tmp/postee/rego-filters/ ================================================ FILE: deploy/kubernetes/postee-actions.yaml ================================================ ## postee-configmap --- kind: ConfigMap apiVersion: v1 metadata: name: postee-config data: cfg.yaml: | --- # The configuration file contains a general settings section, # routes, templates and actions sections. name: tenant # The tenant name aqua-server: # URL of Aqua Server for links. E.g. https://myserver.aquasec.com max-db-size: 1000 # Max size of DB in MB. if empty then unlimited db-verify-interval: 1 # How often to check the DB size. By default, Postee checks every 1 hour # Routes are used to define how to handle an incoming message routes: - name: stdout actions: [ stdout ] template: raw-json - name: actions-route input: contains(input.SigMetadata.ID, "TRC-2") serialize-actions: true # Optional. Serialize actions in route. actions: [save-tracee-event-log, send-event, tag-k8s-resources, kill-vulnerable-pod] template: raw-json # Templates are used to format a message templates: - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json # Actions are target services that should consume the messages actions: - name: stdout type: stdout enable: true # Define a custom action of exec type, that can take params. - name: save-tracee-event-log type: exec enable: true env: ["MY_ENV_VAR=foo_bar_baz", "MY_KEY=secret"] exec-script: | # We create a persistent volume "/postee-artifacts/" to store logs #!/bin/sh echo $POSTEE_EVENT >> /postee-artifacts/tracee.event.log - name: send-event type: http enable: true url: "https://url-to-webhook.com" # Required. URL of the HTTP Request method: POST # Required. Method to use. CONNECT is not supported at this time headers: # Optional. Headers to pass in for the request. "Foo": ["bar", "baz"] timeout: 10s # Optional. Timeout value in XX(s,m,h) body-content: | # Optional. Body of the HTTP request This is an example of sending a Postee Event via an HTTP Action. Event Details: ID: event.input.SigMetadata.ID Date: event.input.SigMetadata.Hostname - name: create-jira-ticket type: jira enable: false url: https://foo-bar.atlassian.com project-key: XYZ # Required. Specify the JIRA product key user: johnwick@example.com # Required. E.g :johndoe@gmail.com" password: hunter2 # Optional. Specify Jira user API key. Used only for Jira Cloud board: "postee-actions" # Optional. Specify the Jira board name to open tickets on labels: ["vulnerability"] # Optional, specify array of labels to add to Ticket, for example: ["label1", "label2"] priority: "High" # Optional. Specify the issues severity. Default is "High" assignee: ["devops@example.com"] # Optional. Specify the assigned user. Default is the user that opened the ticket - name: tag-k8s-resources type: kubernetes enable: true kube-namespace: "default" # Required. Kubernetes namespace to use. kube-label-selector: "statefulset.kubernetes.io/pod-name=event.input.SigMetadata.Hostname" # Required, if specifying labels or annotations. kube-actions: labels: category: "vulnerability" # Required. Label to add. id: event.input.Vulnerability.ID # Optional. It is also possible to add labels based on event inputs. severity: event.input.Vulnerability.Severity - name: kill-vulnerable-pod type: exec enable: true exec-script: | #!/bin/sh PODNAME=$(echo $POSTEE_EVENT | jq -r .SigMetadata.Hostname) curl -k -X DELETE \ -H "Authorization: Bearer $(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ https://kubernetes/api/v1/namespaces/default/pods/$PODNAME ## postee --- apiVersion: v1 kind: Service metadata: labels: app: postee name: postee-svc spec: ports: - name: https port: 8445 protocol: TCP targetPort: 8445 - name: http port: 8082 protocol: TCP targetPort: 8082 selector: app: postee type: ClusterIP --- apiVersion: apps/v1 kind: StatefulSet metadata: labels: app: postee name: postee spec: replicas: 1 selector: matchLabels: app: postee serviceName: postee-svc volumeClaimTemplates: - metadata: name: postee-db spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi - metadata: name: postee-artifacts spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: postee-config spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: rego-templates spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: rego-filters spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi template: metadata: labels: app: postee name: postee spec: initContainers: - name: setting-db image: busybox:1.34 command: - /bin/chown - -R - "1099:1099" - /server volumeMounts: - name: postee-db mountPath: /server/database - name: rego-templates mountPath: /server/rego-templates/custom - name: rego-filters mountPath: /server/rego-filters/custom - name: setting-cfg image: busybox:1.34 command: - cp - /configmap/cfg.yaml - /config/cfg.yaml volumeMounts: - name: configmap-vol mountPath: /configmap readOnly: false - name: postee-config mountPath: /config containers: - image: postee-local:latest imagePullPolicy: IfNotPresent name: postee env: - name: POSTEE_CFG value: /config/cfg.yaml - name: POSTEE_DEBUG value: "not" ports: - name: http containerPort: 8082 protocol: TCP - name: tls containerPort: 8445 protocol: TCP volumeMounts: - name: configmap-vol mountPath: /configmap readOnly: false - name: postee-db mountPath: /server/database - name: postee-config mountPath: /config - name: rego-templates mountPath: /server/rego-templates/custom - name: rego-filters mountPath: /server/rego-filters/custom - name: postee-artifacts mountPath: /postee-artifacts securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 1099 runAsGroup: 1099 resources: limits: cpu: 500m memory: 256Mi requests: cpu: 200m memory: 128Mi livenessProbe: httpGet: path: /ping port: 8082 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /ping port: 8082 initialDelaySeconds: 10 periodSeconds: 10 volumes: - name: configmap-vol configMap: name: postee-config items: - key: cfg.yaml path: cfg.yaml ## postee-ui --- apiVersion: v1 kind: Service metadata: name: postee-ui-svc spec: ports: - name: postee-ui port: 8000 protocol: TCP targetPort: 8000 selector: app: postee-ui type: LoadBalancer --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: postee-ui name: postee-ui spec: replicas: 1 selector: matchLabels: app: postee-ui template: metadata: labels: app: postee-ui spec: initContainers: - name: chmod-er image: busybox:1.34 command: - /bin/chown - -R - "1099:1099" - /config volumeMounts: - name: postee-config mountPath: /config containers: - image: aquasec/postee-ui:latest imagePullPolicy: Always name: postee-ui env: - name: POSTEE_UI_CFG value: /config/cfg.yaml - name: POSTEE_UI_PORT value: "8000" - name: POSTEE_UI_UPDATE_URL value: "http://postee-svc:8082" - name: POSTEE_ADMIN_USER value: admin - name: POSTEE_ADMIN_PASSWORD value: admin ports: - containerPort: 8000 protocol: TCP volumeMounts: - name: postee-db mountPath: /server/database - name: postee-config mountPath: /config securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 1099 runAsGroup: 1099 resources: limits: cpu: 500m memory: 256Mi requests: cpu: 200m memory: 128Mi livenessProbe: httpGet: path: /ping port: 8000 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /ping port: 8000 initialDelaySeconds: 10 periodSeconds: 10 volumes: - name: postee-db persistentVolumeClaim: claimName: postee-db-postee-0 - name: postee-config persistentVolumeClaim: claimName: postee-config-postee-0 ================================================ FILE: deploy/kubernetes/postee-controller.yaml ================================================ ## postee-configmap --- kind: ConfigMap apiVersion: v1 metadata: name: postee-controller-config data: cfg.yaml: | --- actions: - type: stdout name: stdout enable: true - name: my-http-post-from-controller type: http enable: true url: "https://webhook.site/" # Required. URL of the HTTP Request method: POST # Required. Method to use. CONNECT is not supported at this time headers: # Optional. Headers to pass in for the request. "Foo": [ "bar" ] timeout: 10s # Optional. Timeout value in XX(s,m,h) body-content: | # Optional. Body inline content of the HTTP request This is an example of a inline body Input Image: event.input.image - name: my-exec-from-runner runs-on: "postee-runner" type: exec enable: true env: ["MY_ENV_VAR=foo_bar_baz", "MY_KEY=secret"] # Optional. Any environment variables to pass in exec-script: | # Specify the script to run #!/bin/sh echo $POSTEE_EVENT echo "this is hello from postee" routes: - name: stdout actions: [ stdout ] template: raw-json - name: controller-only-route input: contains(input.image, "alpine") actions: [my-http-post-from-controller] template: raw-json - name: runner-only-route input: contains(input.SigMetadata.ID, "TRC-1") serialize-actions: true actions: [my-exec-from-runner, my-http-post-from-runner] template: raw-json - name: controller-runner-route input: contains(input.SigMetadata.ID, "TRC-2") serialize-actions: true # Cannot be strictly guaranteed as executions happen independently on runner/controller actions: [my-exec-from-runner, my-http-post-from-runner, my-http-post-from-controller] template: raw-json templates: - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json server-cert.pem: | -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- server-key.pem: | -----BEGIN PRIVATE KEY----- ----END PRIVATE KEY----- seed-file.txt: | SUAGAA3TNI36JHTD6GLFJRR6KZIY7YXS2ZISHQA4LPZZZG2D6KG5JPV7DM UBUQ63VFZEW3IS7RGQQZF5DIT2FTCMTZAAHFENK3G5M6ADRZ5WAJLAQN root-ca.pem: | -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- ## postee --- apiVersion: v1 kind: Service metadata: labels: app: postee-controller name: postee-controller-svc spec: ports: - name: https port: 8445 protocol: TCP targetPort: 8445 - name: http port: 8082 protocol: TCP targetPort: 8082 - name: nats port: 4222 protocol: TCP targetPort: 4222 selector: app: postee-controller type: ClusterIP --- apiVersion: apps/v1 kind: StatefulSet metadata: labels: app: postee-controller name: postee-controller spec: replicas: 1 selector: matchLabels: app: postee-controller serviceName: postee-controller-svc volumeClaimTemplates: - metadata: name: postee-controller-db spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi - metadata: name: postee-controller-config spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: rego-templates spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: rego-filters spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi template: metadata: labels: app: postee-controller name: postee-controller spec: initContainers: - name: setting-db image: busybox:1.34 command: - /bin/chown - -R - "1099:1099" - /server volumeMounts: - name: postee-controller-db mountPath: /server/database - name: rego-templates mountPath: /server/rego-templates/custom - name: rego-filters mountPath: /server/rego-filters/custom - name: setting-cfg image: busybox:1.34 command: - cp - /configmap/cfg.yaml - /config/cfg.yaml volumeMounts: - name: configmap-vol mountPath: /configmap readOnly: false - name: postee-controller-config mountPath: /config - name: controller-cfg image: busybox:1.34 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'] volumeMounts: - name: configmap-vol mountPath: /configmap readOnly: false - name: postee-controller-config mountPath: /config containers: - image: aquasec/postee:latest imagePullPolicy: Always name: postee-controller args: - "--controller-mode" - "--controller-tls-cert" - "/config/server-cert.pem" - "--controller-tls-key" - "/config/server-key.pem" - "--controller-ca-root" - "/config/root-ca.pem" - "--controller-seed-file" - "/config/seed-file.txt" env: - name: POSTEE_CFG value: /config/cfg.yaml ports: - name: http containerPort: 8082 protocol: TCP - name: tls containerPort: 8445 protocol: TCP - name: nats containerPort: 4222 protocol: TCP volumeMounts: - name: configmap-vol mountPath: /configmap readOnly: false - name: postee-controller-db mountPath: /server/database - name: postee-controller-config mountPath: /config - name: rego-templates mountPath: /server/rego-templates/custom - name: rego-filters mountPath: /server/rego-filters/custom securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 1099 runAsGroup: 1099 resources: limits: cpu: 500m memory: 256Mi requests: cpu: 200m memory: 128Mi livenessProbe: httpGet: path: /ping port: 8082 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /ping port: 8082 initialDelaySeconds: 10 periodSeconds: 10 volumes: - name: configmap-vol configMap: name: postee-controller-config items: - key: cfg.yaml path: cfg.yaml - key: server-cert.pem path: server-cert.pem - key: server-key.pem path: server-key.pem - key: seed-file.txt path: seed-file.txt - key: root-ca.pem path: root-ca.pem ## postee-ui --- apiVersion: v1 kind: Service metadata: name: postee-ui-svc spec: ports: - name: postee-ui port: 8000 protocol: TCP targetPort: 8000 selector: app: postee-ui type: LoadBalancer --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: postee-ui name: postee-ui spec: replicas: 1 selector: matchLabels: app: postee-ui template: metadata: labels: app: postee-ui spec: initContainers: - name: chmod-er image: busybox:1.34 command: - /bin/chown - -R - "1099:1099" - /config volumeMounts: - name: postee-controller-config mountPath: /config containers: - image: aquasec/postee-ui:latest imagePullPolicy: Always name: postee-ui env: - name: POSTEE_UI_CFG value: /config/cfg.yaml - name: POSTEE_UI_PORT value: "8000" - name: POSTEE_UI_UPDATE_URL value: "http://postee-controller-svc:8082" - name: POSTEE_ADMIN_USER value: admin - name: POSTEE_ADMIN_PASSWORD value: admin ports: - containerPort: 8000 protocol: TCP volumeMounts: - name: postee-controller-db mountPath: /server/database - name: postee-controller-config mountPath: /config securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 1099 runAsGroup: 1099 resources: limits: cpu: 500m memory: 256Mi requests: cpu: 200m memory: 128Mi livenessProbe: httpGet: path: /ping port: 8000 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /ping port: 8000 initialDelaySeconds: 10 periodSeconds: 10 volumes: - name: postee-controller-db persistentVolumeClaim: claimName: postee-controller-db-postee-0 - name: postee-controller-config persistentVolumeClaim: claimName: postee-controller-config-postee-0 ================================================ FILE: deploy/kubernetes/postee-runner.yaml ================================================ ## postee-configmap --- kind: ConfigMap apiVersion: v1 metadata: name: postee-runner-config data: client-cert.pem: | -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- client-key.pem: | -----BEGIN PRIVATE KEY----- -----END PRIVATE KEY----- seed-file.txt: | SUAGAA3TNI36JHTD6GLFJRR6KZIY7YXS2ZISHQA4LPZZZG2D6KG5JPV7DM UBUQ63VFZEW3IS7RGQQZF5DIT2FTCMTZAAHFENK3G5M6ADRZ5WAJLAQN root-ca.pem: | -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- ## postee --- apiVersion: v1 kind: Service metadata: labels: app: postee-runner name: postee-runner-svc spec: ports: - name: https port: 18445 protocol: TCP targetPort: 18445 - name: http port: 18082 protocol: TCP targetPort: 18082 selector: app: postee-runner type: ClusterIP --- apiVersion: apps/v1 kind: StatefulSet metadata: labels: app: postee-runner name: postee-runner spec: replicas: 1 selector: matchLabels: app: postee-runner serviceName: postee-svc volumeClaimTemplates: - metadata: name: postee-runner-db spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi - metadata: name: postee-runner-config spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: rego-templates spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: rego-filters spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi template: metadata: labels: app: postee-runner name: postee-runner spec: initContainers: - name: setting-db image: busybox:1.34 command: - /bin/chown - -R - "1099:1099" - /server volumeMounts: - name: postee-runner-db mountPath: /server/database - name: rego-templates mountPath: /server/rego-templates/custom - name: rego-filters mountPath: /server/rego-filters/custom - name: controller-cfg image: busybox:1.34 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'] volumeMounts: - name: configmap-vol mountPath: /configmap readOnly: false - name: postee-runner-config mountPath: /config containers: - image: aquasec/postee:latest imagePullPolicy: Always name: postee-runner args: - "--runner-name" - "postee-runner" - "--controller-url" - "tls://postee-controller-svc.default.svc.cluster.local:4222" - "--runner-tls-cert" - "/config/client-cert.pem" - "--runner-tls-key" - "/config/client-key.pem" - "--runner-ca-root" - "/config/root-ca.pem" - "--runner-seed-file" - "/config/seed-file.txt" - "--url" - "0.0.0.0:18082" - "--tls" - "0.0.0.0:18445" ports: - name: http containerPort: 18082 protocol: TCP - name: tls containerPort: 18445 protocol: TCP volumeMounts: - name: configmap-vol mountPath: /configmap readOnly: false - name: postee-runner-db mountPath: /server/database - name: postee-runner-config mountPath: /config - name: rego-templates mountPath: /server/rego-templates/custom - name: rego-filters mountPath: /server/rego-filters/custom securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 1099 runAsGroup: 1099 resources: limits: cpu: 500m memory: 256Mi requests: cpu: 200m memory: 128Mi livenessProbe: httpGet: path: /ping port: 18082 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /ping port: 18082 initialDelaySeconds: 10 periodSeconds: 10 volumes: - name: configmap-vol configMap: name: postee-runner-config items: - key: client-cert.pem path: client-cert.pem - key: client-key.pem path: client-key.pem - key: seed-file.txt path: seed-file.txt - key: root-ca.pem path: root-ca.pem ================================================ FILE: deploy/kubernetes/postee.yaml ================================================ ## postee-configmap --- kind: ConfigMap apiVersion: v1 metadata: name: postee-config data: cfg.yaml: | --- # Reference: https://github.com/aquasecurity/postee/blob/main/cfg.yaml actions: - type: email name: my-email user: xxxxxx@gmail.com host: smtp.gmail.com port: 587 sender: xxxxxxxx@gmail.com recipients: - xxxxxxxxx@xxxxx.com password: xxxxxxxxxx enable: false - type: serviceNow name: my-service-now enable: false user: xxxxxxxxx password: xxxxxxxxxx instance: xxxxxxxx - type: slack name: my-slack enable: false url: >- https://hooks.slack.com/services/xxxxxxx/xxxxxxx/xxxxxxx - type: teams name: my-teams enable: false url: >- https://xxxxxxxx.webhook.office.com/webhookb2/xxxxxxxx/IncomingWebhook/xxxxx/xxxxx routes: - name: slack-route input: contains(input.image, "alpine") actions: - my-slack template: legacy-slack plugins: aggregate-issues-number: null aggregate-issues-timeout: null policy-show-all: true output: [] - output: [] name: email-route input: | contains(input.image,"alpine") actions: - my-email template: legacy - output: [] name: servicenow-route input: contains(input.image,"alpine") actions: - my-service-now template: legacy - output: [] name: msTeams-route actions: - my-teams template: legacy templates: - name: slack-template rego-package: postee.vuls.slack - name: rego-html rego-package: postee.vuls.html - name: legacy legacy-scan-renderer: html - name: legacy-slack legacy-scan-renderer: slack - name: custom-email url: null - name: trivy-operator-jira rego-package: postee.trivyoperator.jira - name: trivy-operator-slack rego-package: postee.trivyoperator.slack name: tenant AquaServer: https://xxxxxxxxxxx.com ## postee --- apiVersion: v1 kind: Service metadata: labels: app: postee name: postee-svc spec: ports: - name: https port: 8445 protocol: TCP targetPort: 8445 - name: http port: 8082 protocol: TCP targetPort: 8082 selector: app: postee type: ClusterIP --- apiVersion: apps/v1 kind: StatefulSet metadata: labels: app: postee name: postee spec: replicas: 1 selector: matchLabels: app: postee serviceName: postee-svc volumeClaimTemplates: - metadata: name: postee-db spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 1Gi - metadata: name: postee-config spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: rego-templates spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi - metadata: name: rego-filters spec: accessModes: [ "ReadWriteOnce" ] resources: requests: storage: 100Mi template: metadata: labels: app: postee name: postee spec: initContainers: - name: setting-db image: busybox:1.34 command: - /bin/chown - -R - "1099:1099" - /server volumeMounts: - name: postee-db mountPath: /server/database - name: rego-templates mountPath: /server/rego-templates/custom - name: rego-filters mountPath: /server/rego-filters/custom - name: setting-cfg image: busybox:1.34 command: - cp - /configmap/cfg.yaml - /config/cfg.yaml volumeMounts: - name: configmap-vol mountPath: /configmap readOnly: false - name: postee-config mountPath: /config containers: - image: aquasec/postee:latest imagePullPolicy: Always name: postee env: - name: POSTEE_CFG value: /config/cfg.yaml - name: POSTEE_DEBUG value: "not" ports: - name: http containerPort: 8082 protocol: TCP - name: tls containerPort: 8445 protocol: TCP volumeMounts: - name: postee-db mountPath: /server/database - name: postee-config mountPath: /config - name: rego-templates mountPath: /server/rego-templates/custom - name: rego-filters mountPath: /server/rego-filters/custom securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 1099 runAsGroup: 1099 resources: limits: cpu: 500m memory: 256Mi requests: cpu: 200m memory: 128Mi livenessProbe: httpGet: path: /ping port: 8082 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /ping port: 8082 initialDelaySeconds: 10 periodSeconds: 10 volumes: - name: configmap-vol configMap: name: postee-config items: - key: cfg.yaml path: cfg.yaml ## postee-ui --- apiVersion: v1 kind: Service metadata: name: postee-ui-svc spec: ports: - name: postee-ui port: 8000 protocol: TCP targetPort: 8000 selector: app: postee-ui type: LoadBalancer --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: postee-ui name: postee-ui spec: replicas: 1 selector: matchLabels: app: postee-ui template: metadata: labels: app: postee-ui spec: initContainers: - name: chmod-er image: busybox:1.34 command: - /bin/chown - -R - "1099:1099" - /config volumeMounts: - name: postee-config mountPath: /config containers: - image: aquasec/postee-ui:latest imagePullPolicy: Always name: postee-ui env: - name: POSTEE_UI_CFG value: /config/cfg.yaml - name: POSTEE_UI_PORT value: "8000" - name: POSTEE_UI_UPDATE_URL value: "http://postee-svc:8082" - name: POSTEE_ADMIN_USER value: admin - name: POSTEE_ADMIN_PASSWORD value: admin ports: - containerPort: 8000 protocol: TCP volumeMounts: - name: postee-db mountPath: /server/database - name: postee-config mountPath: /config securityContext: allowPrivilegeEscalation: false runAsNonRoot: true runAsUser: 1099 runAsGroup: 1099 resources: limits: cpu: 500m memory: 256Mi requests: cpu: 200m memory: 128Mi livenessProbe: httpGet: path: /ping port: 8000 initialDelaySeconds: 10 periodSeconds: 10 readinessProbe: httpGet: path: /ping port: 8000 initialDelaySeconds: 10 periodSeconds: 10 volumes: - name: postee-db persistentVolumeClaim: claimName: postee-db-postee-0 - name: postee-config persistentVolumeClaim: claimName: postee-config-postee-0 ================================================ FILE: docker-compose.yml ================================================ version: "3" services: posteewebhook: build: context: . dockerfile: Dockerfile ports: - 8082:8082 volumes: - bolt:/server/database - config:/server environment: - "POSTEE_CFG=/server/cfg.yaml" - "POSTEE_DEBUG=ON" posteeui: build: context: . dockerfile: Dockerfile.ui ports: - 8001:8001 volumes: - bolt:/server/database - config:/server environment: - "POSTEE_UI_CFG=/server/cfg.yaml" - "POSTEE_UI_PORT=8001" - "POSTEE_UI_UPDATE_URL=http://posteewebhook:8082" volumes: bolt: config: ================================================ FILE: docs/actions/actions.md ================================================ # Postee Actions ## Motivation Proper 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 ## User Stories In 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. As a, **Postee User** - _I want_, to be able to remove a vulnerable image from my cluster upon a Trivy scan _So that_, I can keep such images unavailable for deployment. - _I want_, to ship Tracee security notification logs from my node when events are detected _So that_, I can build a timelog for forensics purposes. - _I want_, to be able to add labels to my deployments when Starboard detects a vulnerable image in my cluster _So that_, I can effectively tag my resources. ## Configuring Postee Actions In 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. In this case, the incoming security event from Tracee is received by Postee and evaluated by the following route YAML definition: ![img.png](../img/img.png) As 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. ## Exec Action In this case, we call the Exec Action first and then the HTTP Action. They are defined as the following: The Exec Action can take in the following parameters: | Option | Usage | |-------------|-------------------------------------------------------------------------------------------| | env | Optional, custom environment variables to be exposed in the shell of the executing script | | input-file | Required, custom shell script to executed | | exec-script | Required, inline shell script executed | The 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. Below is an example of using `$POSTEE_EVENT`. It uses the inline exec-script script: ![img_3.png](../img/img_3.png) As you can see, we capture the incoming Postee event and write this event to the Tracee event log for forensic purposes. ## HTTP Action Finally, we can configure the Postee HTTP Post Action to ship the captured event logs via our HTTP Action to our remote server. ![img_1.png](../img/img_1.png) | Option | Usage | |----------|-----------------------------------------| | URL | Required, URL of the remote server | | Method | Required, e.g., GET, POST | | Headers | Optional, custom headers to send | | Timeout | Optional, custom timeout for HTTP call | | Bodyfile | Optional, input file for HTTP post body | To run Postee in the container, we can invoke the Postee Docker container: ``` docker run --rm --name=postee \ -v :/config/cfg-actions.yaml \ -e POSTEE_CFG=/config/cfg-actions.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 ``` ## Kubernetes Action In 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: ![img_4.png](../img/img_4.png) | Option | Usage | |---------------------|---------------------------------------------------------------------------------------------------------------------------------| | kube-namespace | Required. Kubernetes namespace to use. | | kube-config-file | Required. Path to .kubeconfig file | | kube-label-selector | Required, if specifying labels or annotations. | | kube-actions | Optional, key-value pair of labels and annotations
Labels must be added via "labels" key and Annotations via "annotations". | ## Docker Action We have also added a Docker Action, that can help you run docker images as an action within a container. ![img_5.png](../img/img_5.png) | Option | Usage | |----------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | docker-image-name | Required. Image name of the docker image. | | docker-cmd | Required. Command to run inside the docker image. | | docker-env | Optional. Environment variables to set in the container. | | docker-network | Optional. Connect the action container to the specified network. {e.g. "host"} | | docker-volume-mounts | Optional*. Volume mounts present inside the container.
* _If you have specified volume mounts, you also need to pass them through into the postee docker container_ | ### Note 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: ``` 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 ``` 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: ``` 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 ``` ================================================ FILE: docs/actions.md ================================================ ## Motivation Proper 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 ## User Stories In 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. As a **Postee User** - _I want_, to be able to remove a vulnerable image from my cluster upon a Trivy scan _So that_, I can keep such images unavailable for deployment. - _I want_, to ship Tracee security notification logs from my node when events are detected _So that_, I can build a timelog for forensics purposes. - _I want_, to be able to add labels to my deployments when Starboard detects a vulnerable image in my cluster _So that_, I can effectively tag my resources. ![settings](img/postee-actions.png) Actions are remote services that messages should be sent to. Each action has two mandatory fields, which are 'name' and 'type'. Key | Description | Values | Example --- | --- | --- | --- *name* | Unique name of the action. This name is used in the route definition. | Any string | teams-action *type* | The type of the action | You can choose from the following types: email, jira, slack, teams, webhook, splunk, serviceNow | email !!! tip Depending on the 'type', additional parameters are required. ## Jira Follow these steps to set up JIRA integration: 1. Get a new token for user: * Login to Jira Cloud. Go to the user profile API tokens (JIRA Cloud users can find it [here](https://id.atlassian.com/manage-profile/security/api-tokens)). * Click on the Create API Token. A new API token for the user is created. * Login to Jira Server/Data center 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. 2. Fill jira action in cfg.yaml: * Jira Cloud: * User: your email. * Password: your API token. * Jira Server/Data center: * User: your UserName. * Password: your Password.\ or * Token: your Personal Access Tokens. Key | Description | Values | Required --------------|----------------------|-----------------|---------- *url* | Jira project url | | Yes *project-key* | The JIRA project key | | Yes *user* | Jira user. Use email for Jira Cloud and UserName for Jira Server/Data Center | | Yes *password* | User's password. API token can also be used for Cloud Jira instances. | | No *token* | User's Personal Access Token. Used only for Jira Server/Data Center | | No *board* | JIRA board key | | No *priority* | ticket priority, e.g., High | | No *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 *issuetype* | issue type, e.g., Bug | | No *labels* | comma separated list of labels that will be assigned to ticket, e.g., ["label1", "label2"]| | No *sprint* | Sprint name, e.g., "3.5 Sprint 8" | | No For Jira you can also specify custom fields that will be populated with values. Use the `unknowns` parameter in cfg.yaml for custom fields. Under the `unknowns` parameter, specify the list of fields **names** to provide value for. Field name can contains spaces. Possible options for getting the field name: ??? note "Get field name from Jira UI" 1. Move to your jira. 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) 3. Click on the required field. ![Field_information](img/jira-field_information.png) 4. Get value from **Name** field. ??? note "Get field name from Jira REST API" 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) 2. Find needed field: ``` ... "id": "customfield_10014", "key": "customfield_10014", "name": "Epic Link", "untranslatedName": "Epic Link", "custom": true, "orderable": true, "navigable": true, "searchable": true, "clauseNames": [ "cf[10014]", "Epic Link" ], "schema": { "type": "any", "custom": "com.pyxis.greenhopper.jira:gh-epic-link", "customId": 10014 } }, ... ``` 3. Get value from **Name** field. Example of using the `unknowns` parameter in the cfg.yaml file: ```yaml unknowns: Epic Link: "K8S-1" ``` !!! tip 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. For example: ```yaml unknowns: mycustom: "this is a text custom field" mycustom-numeric-field: 123 mycustom-multiple-value: 1,2,3 mycustom-multiple-line-text-field: "text \n moretext" mycustom-date-time-picker: 2014-04-11T12:14:26.880+0400 mycustom-url: https://tour.golang.org/moretypes/7 ``` ## Email Key | Description | Values | Required -------------|-------------|-----------------|---------- *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 | *user* | if auth supported. User name (usually email address) | | No *password* | if auth supported. Password | | No *host* | SMTP host name | | Yes *port* | SMTP port | | Yes *sender* | Sender's email address | | Yes *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 ## Slack Getting the Slack webhooks [Create a Slack Custom App](https://api.slack.com/messaging/webhooks). Copy webhook url to the Postee config Key | Description | Values --- | --- | --- *url* | Slack WebHook URL (includes the access key) | ## MS Teams Open your Microsoft Teams client. Click on the "..." near the channel you would like to send notifications to. Choose "Connectors". The connectors window will open. Look for the "Incoming Webhook" connector (it is under the "All" category). Click "Add" near the Incoming Webhook connector. Click "Add" again. Provide a name and click "Create". You will be provided with a URL address. Copy this URL and put it in the cfg.yaml. Key | Description | Values --- | --- | --- *url* | MS Teams WebHook URL | ## Splunk You will need to care about an HTTP Event Collector in Splunk Enterprise or Splunk Cloud. !!! tip This can usually be found in the Splunk console under "Settings -> Data Inputs -> HTTP Event Collector -> Add New". Once you create an HTTP Event Collector you will receive a token. You should provide this token, together with the Splunk HTTP Collector URL, as part of the cfg.yaml settings. Key | Description | Values | Required ------------ | ------------------------------------------------------------------ | --------------- | --------- *token* | The Splunk HTTP event collector token | | Yes *url* | URL to Splunk HTTP event collector (e.g. http://server:8088) | | Yes *size-limit* | Maximum scan length, in bytes. Default: 10000 | 10000 | | No ## ServiceNow Key | Description | Values | Required ---------- | ------------------------------------------------------------------ | --------------- | --------- *user* | ServiceNow user name | | Yes *password* | User API key / password | | Yes *instance* | Name of ServiceNow Instance (usually the XXX at XXX.servicenow.com)| | Yes *board* | ServiceNow board name to open tickets on. Default is "incident" | | ## Nexus IQ Key | Description | Values | Required ----------------- | -------------------------------------------------------- | --------------- | -------- *user* | Nexus IQ user name | | Yes *password* | Nexus IQ password | | Yes *url* | Url of Nexus IQ server | | Yes *organization-id* | Organization UID like "222de33e8005408a844c12eab952c9b0" | | Yes ## Dependency Track Key | Description | Values | Required -------------------------- | ------------------------------ | ------- | -------- *url* | Url of Dependency Track | | Yes *dependency-track-api-key* | API key of Dependency Track | | Yes ## OpsGenie ??? note "Set up OpsGenie and get a token" 1. Go to your Opsgenie and select Teams from menu. 2. Select your team to access your team dashboard. 3. Select Integrations from left navigation. 4. Select Add Integration. 5. Select API Integration. 6. Copy `API Key`. 7. When done with all configurations, select Save Integration to enable the integration. See more details here: [Set up an integrated tool for Opsgenie](https://support.atlassian.com/opsgenie/docs/set-up-an-integrated-tool/). !!! caution 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. If the integration assigns an alert to a team, it can only create alerts for that team. 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. Key | Description | Values | Required -----------| --------------------------------------- | ----------------|--------- token | an API key from an API integration | | Yes user | Display name of the request owner. | | No assignee | Comma separated list of users that the alert will be routed to send notifications | | No recipients | Comma separated list of users that the alert will become visible to without sending any notification | | No priority | Specify the alert priority. Default is "P3" | "P1" "P2" "P3" "P4" "P5"| No tags | Comma separated list of the alert tags. | | No alias | Client-defined identifier of the alert. | | No entity | Entity field of the alert that is generally used to specify which domain alert is related to. | | No ## Exec Option | Usage | Required -------------|-------------------------------------------------------------------------------------------|---------- env | custom environment variables to be exposed in the shell of the executing script | No input-file | custom shell script to executed | Yes exec-script | inline shell script executed | Yes The 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. Below is an example of using `$POSTEE_EVENT`. It uses the inline exec-script script: ![img_3.png](img/img_3.png) ## HTTP ![img_1.png](img/img_1.png) Option | Usage | Required ----------|-----------------------------------------|---------- URL | URL of the remote server | Yes Method | e.g., GET, POST | Yes Headers | custom headers to send | No Timeout | custom timeout for HTTP call | No Bodyfile | input file for HTTP post body | No ## Kubernetes ![img_4.png](img/img_4.png) Option | Usage | Required ---------------------|---------------------------------------------------------------------------------------------------------------------------------|---------- kube-namespace | Kubernetes namespace to use. | Yes kube-config-file | Path to .kubeconfig file | Yes kube-label-selector | if specifying labels or annotations. | Yes kube-actions | key-value pair of labels and annotations
Labels must be added via "labels" key and Annotations via "annotations". | No ## Docker ![img_5.png](img/img_5.png) Option | Usage | Required ----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|---------- docker-image-name | Image name of the docker image. | Yes docker-cmd | Command to run inside the docker image. | Yes docker-env | Environment variables to set in the container. | No docker-network | Connect the action container to the specified network. {e.g. "host"} | No docker-volume-mounts | *Volume mounts present inside the container.
* _If you have specified volume mounts, you also need to pass them through into the postee docker container_ | No !!! note 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: ``` 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 ``` !!! tip 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: ``` 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 ``` ## Generic Webhook Key | Description | Values --- | --- | --- *url* | Webhook URL | *timeout* | Webhook timeout | !!! tip The 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) ## DefectDojo DefectDojo 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. At 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. The 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. The 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). ### Implementation 1. DefectDojo - create an non-interactive API user and an API token 2. Postee - deploy the token as `DEFECTDOJO_API_TOKEN` environment variable 3. Postee - deploy the base URL for DefectDojo using `DEFECTDOJO_URL` 4. Mount the [example shell script](../actions/example/exec/defectdojo-curl-upload-scan.sh) into the container 5. Mount the [example rego template](../rego-templates/example/defectdojo/trivy-operator-defectdojo.rego) into the container 6. Update your configuration according to the [example](../config/cfg-trivy-operator-defectdojo.yaml) provided 7. 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` ================================================ FILE: docs/advanced.md ================================================ This page covers some advanced topics that the experienced users of Postee might like to try. ## Using environment variables in Postee Configuration File Postee supports use of environment variables for *Output* fields: **User**, **Password** and **Token**. Add prefix `$` to the environment variable name in the configuration file, for example: ``` actions: - name: my-jira type: jira enable: true user: $JIRA_USERNAME token: $JIRA_SERVER_TOKEN ``` ### Helm When installing Postee on Kubernetes with Helm, you can provide environment variables from Kubernetes secrets. Given there is a Secret containing sensitive information: ``` apiVersion: v1 kind: Secret metadata: name: mysecret type: Opaque data: JIRA_USERNAME: secret-username JIRA_SERVER_TOKEN: secret-token ``` You can refer to this secret and use its data in Postee by specifying its name in the Helm values: ``` envFrom: - mysecret ``` ## Customizing Templates 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. To create your own template, you should create a new file under the 'rego-templates' directory, and use the [Rego language](https://www.openpolicyagent.org/docs/latest/policy-language/) for the actual template code. Message 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. The following variables should be defined in the custom Rego template. Key | Description |Type --- | --- | --- *result* | message body| Can be either string or json *title* | message title| string *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 So the simplest example of Rego template would look like: ```rego package example.vuls.html title:="Vulnerabilities are found" result:=sprintf("Vulnerabilities are found while scanning of image: %s", [input.image]) ``` Two 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 ## Data Persistence The Postee container uses BoltDB to store information about previously scanned images. This is used to prevent resending messages that were already sent before. The size of the database can grow over time. Every image that is saved in the database uses 20K of storage. Postee 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". By default, the directory for the database file is “/server/database/webhook.db”. !!! tip 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. The "deploy/kubernetes" directory in this project contains an example deployment that includes a basic Host Persistency. ================================================ FILE: docs/aquacloud.md ================================================ ## Configure the Aqua Server with Webhook Integration Postee can be integrated with Aqua Console to deliver vulnerability and audit messages to target systems. You can configure the Aqua Server to send a Webhook notification whenever a new vulnerability is found. Navigate to the **Image Scan Results Webhook** page, under the "Settings" menu. ![Screenshot](img/webhook-integration.png) Click "Enable sending image scan results to webhook", and specify the URL of Postee. Now, scan an image and look at the Postee log files - you will see that Postee have received an incoming message once scan is done, and that the message was routed based on the cfg.yaml configuration. You can also configure the Aqua Server to send a Webhook notification for every audit message. Navigate to the **Log Management** page, under the "Integrations" menu. ![Screenshot](img/aqua-webhook-audit.jpg) Click on the "Webhook" item, and specify the URL of Postee. Now every audit event in Aqua will be sent to Postee. You can configure routes and input message conditions in Postee cfg.yaml to forward appropriate messages to target systems. The **Postee URL** is in the following formats: > `https://:8445` > `http://:8082` !!! tip For more details about the Postee URL installed using kubernetes [click here](./deploy/kubernetes/README.md) ### Validate the Integration To 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). When 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. !!! tip To troubleshoot the integration, you can look at both the Aqua Postee container logs and the Aqua Server logs. Use the "docker logs " command to view these logs.* ================================================ FILE: docs/blueprints/devops-pagerduty.md ================================================ # Paging DevOps Teams ## Introduction In 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. ## Scenario A 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. They 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. ![img.png](assets/trivy-pagerduty.png) ## Sample Configs In this case a sample configuration for the components can be described as follows: ### Postee Config ```yaml routes: - name: Trivy Alerts to Pagerduty input: input.report.summary.criticalCount > 0 actions: [alert-devops] template: trivy-raw-json # Templates are used to format a message templates: - name: trivy-raw-json rego-package: postee.rawmessage.json # Actions are target services that should consume the messages actions: - name: alert-devops type: pagerduty enable: true pagerduty-auth-token: "" pagerduty-routing-key: "" ``` ================================================ FILE: docs/blueprints/external-healthcheck.md ================================================ # Distributed Service Healthcheck ## Introduction In 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. ## Scenario A DevOps operator gets paged about a service that they maintain. This page turns out to be flaky and non-actionable. In 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. By 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. ![img.png](assets/distributed-healthcheck.png) ## Sample Configs In this case a sample configuration for the components can be described as follows: ### Postee Config Postee 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. ```yaml routes: - name: actions-route input: contains(input.ServiceURL.Reachable, "false") actions: [send-slack-message, eu-check, apac-check, na-check] # Outputs are target services that should consume the messages actions: - name: send-slack-messsage type: slack enable: true url: https://hooks.slack.com/services/TAAAA/BBB/ - name: eu-check type: http enable: true url: "https://.lambda-url..on.aws/" method: POST - name: apac-check type: http enable: true url: "https://.lambda-url..on.aws/" method: POST - name: na-check type: http enable: true url: "https://.lambda-url..on.aws/" method: POST ``` ### Lambda Config ```python import os from datetime import datetime from urllib.request import Request, urlopen SITE = "https://www.my-highly-available-website.com" EXPECTED = "Text I expect to find" def validate(res): return EXPECTED in res def lambda_handler(event, context): timeNow = datetime.today().strftime('%Y-%m-%d-%H:%M:%S') print('Checking {} at {}...'.format(SITE, timeNow)) try: req = Request(SITE, headers={'User-Agent': 'AWS Lambda'}) if not validate(str(urlopen(req).read())): raise Exception('Validation failed') except: print('Check failed!') raise else: print('Check passed!') return ('aquasec.com is up! at {}'.format(timeNow)) finally: print('Check complete at {}'.format(str(datetime.now()))) ``` Inspired by: https://github.com/amazon-archives/serverless-app-examples/blob/master/python/lambda-canary/lambda_function.py ================================================ FILE: docs/blueprints/image-processing.md ================================================ # Doing Serverless Image Recognition using Postee Actions and AWS ## Introduction In 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. ## Scenario A 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. Upon successful identification, the user should be informed of such an event. ![img.png](assets/image-rekognition.png) ## Sample Configs In this case a sample configuration for the components can be described as follows: ### Postee Config ```yaml routes: - name: actions-route input: contains(input.Camera.Event, "Finding") serialize-actions: true actions: [send-slack-message, process-image] # Outputs are target services that should consume the messages actions: - name: send-slack-messsage type: slack enable: true url: https://hooks.slack.com/services/TAAAA/BBB/ - name: process-image type: http enable: true url: "https://.lambda-url..on.aws/" method: POST ``` ### AWS Rekognition & Lambda Config The 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 In 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. ```python import boto3 from rekognition_objects import ( RekognitionFace, RekognitionVideo ) def do_face_detection(self): return self._do_rekognition_job( "face detection", self.rekognition_client.start_face_detection, self.rekognition_client.get_face_detection, lambda response: [ RekognitionFace(face['Face'], face['Timestamp']) for face in response['Faces']]) def lambda_handler(event, context): rekognition_client = boto3.client('rekognition') video = RekognitionVideo.from_event(event, rekognition_client) faces = video.do_face_detection() return faces ``` ================================================ FILE: docs/blueprints/trivy-aws-security-hub.md ================================================ # Trivy AWS CSPM Scanning ## Introduction In 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. ## Scenario A 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. They 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. ![img.png](assets/trivy-aws-postee.png) ## Sample Configs In this case a sample configuration for the components can be described as follows: ### Postee Config Postee 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. ```yaml actions: - type: awssecurityhub enable: true name: Send Findings to Security Hub routes: - name: Send Trivy Findings to AWS Security Hub template: raw-json actions: - Send Findings to Security Hub input-files: - Trivy AWS Findings templates: - name: raw-json rego-package: postee.rawmessage.json rules: - name: Trivy AWS Findings name: Send Trivy Results to AWS Security Hub ``` !!! note Currently Postee AWS Security Hub configuration only supports reading AWS Credentials from the AWS config file present on disk. ### AWS Security Hub configuration AWS Security Hub can be configured using the instructions as defined [here](https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-settingup.html) !!! tip 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. ### Trivy Webhook Plugin [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: #### Install the plugin ```shell trivy plugin install github.com/aquasecurity/trivy-plugin-webhook ``` #### Run the Trivy scan using the plugin ```shell trivy webhook -- --url= -- ``` ================================================ FILE: docs/blueprints/trivy-operator.md ================================================ # Trivy Operator ## Introduction In 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. ## Scenario A 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. They 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. ![img.png](assets/trivy-operator-webhook.png) ## Sample Configs In this case a sample configuration for the components can be described as follows: ### Postee Config ```yaml routes: - name: Trivy Operator Alerts input: input.report.summary.criticalCount > 0 # You can customize this based on your needs actions: [send-slack-msg] template: trivy-operator-slack # Templates are used to format a message templates: - name: trivy-operator-slack rego-package: postee.trivyoperator.slack # Actions are target services that should consume the messages actions: - name: send-slack-msg type: slack enable: true url: ``` If all goes well, you should see a report in your Slack channel next time it is generated. ![img.png](assets/trivy-operator-slack-report.png) ================================================ FILE: docs/blueprints/trivy-vulnerability-scan.md ================================================ # Trivy Vulnerability Scan ## Introduction In 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. A video format of this guide is also available [here](https://youtu.be/HZ5Z8jAVH8w?t=420). ## Scenario A 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. They 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. ![img.png](../img/trivy-postee.png) ## Sample Configs In this case a sample configuration for the components can be described as follows: ### Postee Config ```yaml routes: - name: trivy-alpine-vulns input: contains(input.Metadata.OS.Family, "alpine") actions: [my-jira] template: trivy-raw-json # Templates are used to format a message templates: - name: trivy-raw-json rego-package: postee.rawmessage.json # Actions are target services that should consume the messages actions: - name: my-jira type: jira enable: true url: "https://foo.bar.com" user: "jdoe@foo.bar.com" password: "hunter2" project-key: "ABC" board: "Backlog" labels: ["trivy-vulns"] ``` ### Trivy Webhook Plugin [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: #### Install the plugin ```shell trivy plugin install https://github.com/aquasecurity/trivy-plugin-webhook ``` #### Run the Trivy scan using the plugin ```shell trivy webhook -- --url= -- ``` ================================================ FILE: docs/config.md ================================================ 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: 1. [General settings](/postee/settings) 2. [Routes](/postee/routes) 3. [Templates](/postee/templates) 4. [Actions](/postee/actions) These sections will be described in detail as we proceed through the documentation. ================================================ FILE: docs/controller-runner.md ================================================ # Controller Runner Mode ## Introduction Postee can also be run in Controller/Runner mode. The idea is to decouple enforcement from execution, if applicable. ## Scenario In 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. In 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. ![img.png](img/controller-runner.png) ## Configuration ### Run Postee in Controller mode: ```shell postee --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" ``` | Option | Required | Description | |----------------------|------------------------------|----------------------------------------| | controller-mode | true | Enable Postee to run as a Controller | | controller-ca-root | false | TLS CA Root Certificate for Controller | | controller-tls-cert | false | TLS Certificate for Controller | | controller-tls-key | false | TLS Key for Controller | | controller-seed-file | false | Seed file for Controller | ??? note "Example Controller/Runner Configuration" ```yaml name: Postee Controller Runner Demo routes: - name: controller-only-route input: contains(input.image, "alpine") actions: [my-http-post-from-controller] template: raw-json - name: runner-only-route input: contains(input.SigMetadata.ID, "TRC-1") serialize-actions: true actions: [my-exec-from-runner, my-http-post-from-runner] template: raw-json - name: controller-runner-route input: contains(input.SigMetadata.ID, "TRC-2") actions: [my-exec-from-runner, my-http-post-from-runner, my-http-post-from-controller] template: raw-json templates: - name: raw-json rego-package: postee.rawmessage.json actions: - name: stdout type: stdout enable: true - name: my-http-post-from-controller type: http enable: true url: "https://webhook.site/" method: POST headers: "Foo": [ "bar" ] timeout: 10s body-content: | This is an example of a inline body Input Image: event.input.image - name: my-exec-from-runner runs-on: "postee-runner-1" type: exec enable: true env: ["MY_ENV_VAR=foo_bar_baz", "MY_KEY=secret"] exec-script: | #!/bin/sh echo $POSTEE_EVENT echo "this is hello from postee" - name: my-http-post-from-runner runs-on: "postee-runner-1" type: http enable: true url: "https://webhook.site/" method: POST body-content: | This is an another example of a inline body Event ID: event.input.SigMetadata.ID ``` The only notable change in the configuration as defined is of the Actions that can run on Runners. Observe the `runs-on` clause below. ```yaml - name: my-exec-from-runner runs-on: "postee-runner-1" type: exec enable: true exec-script: | #!/bin/sh echo $POSTEE_EVENT echo "this is hello from postee" ``` In this case this particular Action will run on Postee Runner that identifies itself as `postee-runner-1` ### Run Postee in Runner mode: ```shell postee --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 ``` | Option | Required | Description | |------------------|--------------------------|----------------------------------------------------------| | controller-url | true | The URL to the Postee Controller | | runner-name | true | The Name of the Runner, as defined in configuration YAML | | runner-ca-root | false | TLS Root CA Certificate for Runner | | runner-tls-cert | false | TLS Certificate for Runner | | runner-tls-key | false | TLS Key for Runner | | runner-seed-file | false | Seed file for Runner | ### Secured Controller/Runner Channel The communication channel between Controller and Runner can be optionally secured with TLS and be Authentication (AuthN). TLS 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. AuthN 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. A 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`. This can be helpful in situations where Postee Config contains secrets that are configured in an Action that runs on a Runner. ## Walkthrough In 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. Postee 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. The 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. ## Additional Info Postee 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. All events received by Postee Runners are reported upstream to the Controller. This has two benefits: 1. Executions and Events received by the Runners can be monitored at a central level (Controller). 2. Mixing of Runner and Controller Actions within a single Route, for ease of usage. Mixing of Runner and Controller Actions can be explained with a following sample configuration: ```yaml - name: controller-only-route input: contains(input.image, "alpine") actions: [my-slack-message-from-controller] template: raw-json - name: runner-only-route input: contains(input.SigMetadata.ID, "TRC-1") serialize-actions: true actions: [my-exec-from-runner, my-http-post-from-runner] template: raw-json - name: controller-runner-route input: contains(input.SigMetadata.ID, "TRC-2") serialize-actions: true actions: [my-exec-from-runner, my-http-post-from-runner, my-jira-ticket-from-controller] template: raw-json ``` In 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. In 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. #### A quick note on Serialization The 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). ================================================ FILE: docs/demo.md ================================================ 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. In this case, the incoming security event from Tracee is received by Postee and evaluated by the following route YAML definition: ![img.png](img/img.png) As 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. ## Exec Action In this case, we call the Exec Action first and then the HTTP Action. They are defined as the following: The Exec Action can take in the following parameters: | Option | Usage | |-------------|-------------------------------------------------------------------------------------------| | env | Optional, custom environment variables to be exposed in the shell of the executing script | | input-file | Required, custom shell script to executed | | exec-script | Required, inline shell script executed | The 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. Below is an example of using `$POSTEE_EVENT`. It uses the inline exec-script script: ![img_3.png](img/img_3.png) As you can see, we capture the incoming Postee event and write this event to the Tracee event log for forensic purposes. ## HTTP Action Finally, we can configure the Postee HTTP Post Action to ship the captured event logs via our HTTP Action to our remote server. ![img_1.png](img/img_1.png) | Option | Usage | |----------|-----------------------------------------| | URL | Required, URL of the remote server | | Method | Required, e.g., GET, POST | | Headers | Optional, custom headers to send | | Timeout | Optional, custom timeout for HTTP call | | Bodyfile | Optional, input file for HTTP post body | To run Postee in the container, we can invoke the Postee Docker container: ``` docker run --rm --name=postee \ -v :/config/cfg-actions.yaml \ -e POSTEE_CFG=/config/cfg-actions.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 ``` ================================================ FILE: docs/deployment.md ================================================ # Deployment ## Kubernetes Due 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. ```yaml # BUG: postee-0 und posteeui both need access to the same PVC (database) so we need to ensure both run on the same node affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: app.kubernetes.io/instance operator: In values: - postee topologyKey: kubernetes.io/hostname ``` ================================================ FILE: docs/examples.md ================================================ Here are some Postee configuration samples to showcase a variety of use cases. ??? example "Forward all "Block" audit events" ```yaml name: myserver aqua-server: https://myserver.com max-db-size: 1000MB delete-old-data: 100 db-verify-interval: 1 routes: - name: team-drift input: input.level = "block" actions: [my-teams] template: raw-html actions: - name: my-teams type: teams enable: true url: https://outlook.office.com/webhook/ templates: - name: raw-html rego-package: postee.rawmessage.html ``` ??? example "Forward Critical vulnerabilities" ```yaml # This example will forward events of images with critical vulnerabilities to MS Teams. # Note that duplicate events of same image will be ignored for 30 days. name: myserver aqua-server: https://myserver.com max-db-size: 1000MB delete-old-data: 100 db-verify-interval: 1 routes: - name: team-critical-vul input: input.vulnerability_summary.critical > 0 actions: [my-teams] template: raw-html plugins: unique-message-props: ["digest","image","registry", "vulnerability_summary.high", "vulnerability_summary.medium", "vulnerability_summary_low"] unique-message-timeout: 30d actions: - name: my-teams type: teams enable: true url: https://outlook.office.com/webhook/ templates: - name: raw-html rego-package: postee.rawmessage.html ``` ??? example "Forward Drift events" ```yaml # This example will forward events of Drift Prevention to MS Teams. name: myserver aqua-server: https://myserver.com max-db-size: 1000MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited delete-old-data: 100 # delete data older than N day(s). If empty then we do not delete. db-verify-interval: 1 # hours. an Interval between tests of DB. Default: 1 hour routes: - name: team-drift input: contains(input.control, "Drift") actions: [my-teams] template: raw-html actions: - name: my-teams type: teams enable: true url: https://outlook.office.com/webhook/ templates: - name: raw-html # Raw message json rego-package: postee.rawmessage.html # HTLM template REGO package ``` ??? example "Add Kubernetes Labels and Annotations" ```yaml name: tenant aqua-server: max-db-size: 1000MB db-verify-interval: 1 routes: - name: stdout actions: [ stdout ] template: raw-json - name: actions-route input: contains(input.SigMetadata.ID, "TRC-2") actions: [my-k8s] template: raw-json templates: - name: raw-json rego-package: postee.rawmessage.json actions: - name: stdout type: stdout enable: true - name: my-k8s type: kubernetes enable: true kube-namespace: "default" kube-config-file: "/path/to/kubeconfig" kube-label-selector: "app=nginx-app" kube-actions: labels: foo-label: "bar-value" bar-label: event.input.SigMetadata.ID annotations: foo-annotation: "bar-value" bar-annotation: event.input.SigMetadata.ID ``` ??? example "Run ad-hoc docker image" ```yaml name: tenant aqua-server: max-db-size: 1000MB db-verify-interval: 1 routes: - name: stdout actions: [ stdout ] template: raw-json - name: actions-route input: contains(input.SigMetadata.ID, "TRC-2") actions: [stop-vulnerable-pod] template: raw-json templates: - name: raw-json rego-package: postee.rawmessage.json actions: - name: stdout type: stdout enable: true - name: stop-vulnerable-pod type: docker enable: true docker-image-name: "bitnami/kubectl:latest" docker-cmd: ["delete", "pod", event.input.SigMetadata.hostname] docker-network: "host" docker-volume-mounts: "path/to/.kube/config": "/.kube/config" ``` ??? example "Collect and send logs" ```yaml name: tenant aqua-server: localhost max-db-size: 1000MB db-verify-interval: 1 routes: - name: stdout actions: [ stdout ] template: raw-json - name: actions-route input: contains(input.SigMetadata.ID, "TRC-2") serialize-actions: true actions: [my-exec, my-http-post-file, my-http-post-content] template: raw-json templates: - name: raw-json rego-package: postee.rawmessage.json actions: - name: stdout type: stdout enable: true - name: my-exec type: exec enable: true env: ["MY_ENV_VAR=foo_bar_baz", "MY_KEY=secret"] exec-script: | #!/bin/sh echo $POSTEE_EVENT >> /tmp/postee.event.logs - name: my-http-post-file type: http enable: true url: "https://my-fancy-url.com" method: POST body-file: /tmp/postee.event.logs - name: my-http-post-content type: http enable: true url: "https://my-fancy-url.com" method: POST headers: "Foo": [ "bar" ] "Haz": [ "baz" ] timeout: 10s body-content: | This is an example of a inline body Event ID: event.input.Signature.ID ``` ================================================ FILE: docs/improvements.md ================================================ # Improvements Postee like any other software isn't perfect and as the writing of this document can be improved in the following areas: ## UI This 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. ![img.png](img/postee-ui-drag-and-drop.png) The 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. ## Alternate Policy language Today 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. Therefore, 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: 1. Golang Policies 2. [CEL-Go](https://github.com/google/cel-go) ## Support for more Actions Today Postee supports a wide variety of Postee Actions but the list can be further expanded by including the following: 1. AWS Cloudwatch Logs 2. Azure automation 3. GCP automation Extending 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. [Link to example PR](https://github.com/aquasecurity/postee/pull/468) ## My idea is not listed here Do 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. ================================================ FILE: docs/index.md ================================================ #
![Postee Logo](img/postee.png){ align="center" }
Postee is a simple message routing application that receives input messages through a webhook interface, and can take enforce actions using predefined outputs via integrations. Primary 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. In addition, Postee can also be used to enforce pre-defined behaviours that can orchestrate actions based on input messages as triggers. ![Postee v2 scheme](img/postee-v2-scheme.png) ================================================ FILE: docs/install.md ================================================ To run Postee you will first need to configure the [Postee Configuration File](/postee/config), which contains all the message routing logic. After the configuration file is ready, you can run the official Postee container image: **aquasec/postee:latest**, or compile it from source. There 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. After 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. ### Docker To run Postee as a Docker container, you mount the cfg.yaml to '/config/cfg.yaml' path in the Postee container. ```bash docker run -d --name=postee -v //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 aquasec/postee:latest ``` ### Kubernetes When running Postee on Kubernetes, the configuration file is passed as a ConfigMap that is mounted to the Postee pod. #### Cloud Providers ``` bash kubectl create -f https://raw.githubusercontent.com/aquasecurity/postee/main/deploy/kubernetes/postee.yaml ``` #### Using HostPath ``` bash kubectl create -f https://raw.githubusercontent.com/aquasecurity/postee/main/deploy/kubernetes/hostPath/postee-pv.yaml ``` !!! Note "Persistent Volumes Explained" - `postee-db`: persistent storage directory `/server/database` - `postee-config`: mount the cfg.yaml to a writable directory `/config/cfg.yaml` - `postee-rego-templates`: mount custom rego templates - `postee-rego-filters`: mount custom rego filters To edit the default Postee-UI user ``` kubectl -n postee set env deployment/my-posteeui -e POSTEE_ADMIN_USER=testabc -e POSTEE_ADMIN_PASSWORD=password ``` The Postee endpoints ``` http://postee-svc.default.svc.cluster.local:8082 ``` ``` https://postee-svc.default.svc.cluster.local:8445 ``` The Postee-UI endpoint ```` http://postee-ui-svc.default.svc.cluster.local:8000 ```` #### Controller/Runner To use Controller/Runner functionality within Kubernetes, you can follow a reference manifest implementation: - [Controller](https://github.com/aquasecurity/postee/blob/main/deploy/kubernetes/postee-controller.yaml) - [Runner](https://github.com/aquasecurity/postee/blob/main/deploy/kubernetes/postee-runner.yaml) ### Helm When running Postee on Kubernetes, the configuration file is passed as a ConfigMap that is mounted to the Postee pod. This chart bootstraps a Postee deployment on a [Kubernetes](https://kubernetes.io/) cluster using the [Helm package manager](https://helm.sh/). #### Prerequisites - Kubernetes 1.17+ - Helm 3+ #### Test the Chart Repository ```bash cd deploy/helm helm install my-postee -n postee --dry-run --set-file applicationConfigPath="../../cfg.yaml" ./postee ``` #### Installing the Chart from the Source Code ```bash cd deploy/helm helm install app --create-namespace -n postee ./postee ``` #### Installing from the the Aqua Chart Repository Let's add the Helm chart and deploy Postee executing: ```bash helm repo add aquasecurity https://aquasecurity.github.io/helm-charts/ helm repo update helm search repo postee helm install app --create-namespace -n postee aquasecurity/postee ``` Check that all the pods are in Running state: `kubectl get pods -n postee` We check the logs: ``` kubectl logs deployment/my-posteeui -n postee | head ``` ``` kubectl logs statefulsets/my-postee -n postee | head ``` #### Delete Chart ```bash helm -n postee delete my-postee ``` #### From Source Clone and build the project: ```bash git clone git@github.com:aquasecurity/postee.git make build ``` After that, modify the cfg.yaml file and set the 'POSTEE_CFG' environment variable to point to it. ```bash export POSTEE_CFG= ./bin/postee ``` ================================================ FILE: docs/routes.md ================================================ 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. The 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. ![settings](img/postee-email-route.png) !!! tip See the complete Rego Language in [OPA-reference](https://www.openpolicyagent.org/docs/latest/policy-reference/#built-in-functions) After 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. The below table describes the fields to define a route: Key | Description | Possible Values | Example --- | --- |-------------------------------------------------------| --- *name*|Unique name of route| string | teams-vul-route *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") *input-files*|One or more files with Rego rules| Set of Rego language files | ["Policy-Registry.rego", "Policy-Min-Vulnerability.rego"] *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"]. *serialize-actions*|Serialize the list of actions| true, false(default) |true *template*| A template that is defined in the "template" section| any template name | raw-html The `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". To 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. ``` allow{ PermitImageNames PermitMinVulnerability } ``` If you are using your own rego files, then the **package** field should be "postee" and the result should be in the **allow** function: ``` package postee your_function{...} # 0 or more your functions allow { your_function } ``` For example, the following input definition will match JSON messages that have 'image.name' field with value that contains the string 'alpine': ``` input: contains(input.image,"alpine") ``` Another example using regular expression: ``` input: regex.match("alp:*", input.image) ``` You 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. ``` input: | contains(input.image,"alpine") contains(input.registry, "Docker Hub") input.vulnerability_summary.critical>0 ``` ## Postee Route Configuration You could use Postee with any json. See the following example receiving json events: ### Route All Messages To create a route that matches all messages, simply use the following: ``` routes: - name: catch-all input: input ... ``` ### Route Drift Prevention Messages To create a route that matches only messages that originated from a "Drift Prevention" event, use the following: ``` routes: - name: catch-drift input: contains(input.control, "Drift") ... ``` ### Route Tracee Message The following input JSON message is from [Tracee](https://github.com/aquasecurity/tracee). Set `input` property of route to: `contains(input.SigMetadata.ID,"TRC-")` to limit the route to handle Tracee messages only In the section [rego-templates](https://github.com/aquasecurity/postee/tree/main/rego-templates) have rego templates samples to use with Tracee: - tracee-html.rego - tracee-slack.rego ### Plugins 'Plugins' section contains configuration for useful Postee features. Key | Description | Possible Values | Example --- | --- | --- | --- *aggregate-message-number*|Number of messages to aggregate into one message.| any integer value | 10 *aggregate-message-timeout*|number of seconds, minutes, hours to aggregate|Maximum is 24 hours Xs or Xm or Xh | 1h *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"]``` *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 ================================================ FILE: docs/settings.md ================================================ General settings are specified at the root level of cfg.yaml. They include general configuration that applies to the Postee application. ![settings](img/postee-settings.png) Key | Description | Possible Values | Example Value --- |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- | --- *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 *db-verify-interval*| Specify time interval (in hours) for Postee to perform database cleanup jobs. Default: 1 hour | any integer value | 1 *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 ================================================ FILE: docs/templates.md ================================================ 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. Each template has a `name` field, which is used by the route to assign the template to input and output. !!! tip Use the default Legacy template "html" for general output ![settings](img/postee-template-default.png) In addition to name, a template will have **one** of the 4 below keys: Key | Description | Example --- | --- | --- *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` *body*| Specify inline template. Relative small templates can be added to config directly | input *url*| Load from url. Rego template can be loaded from url.| http://myserver.com/rego.txt *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 !!! tip Pre made examples for templates can be found [here](https://github.com/aquasecurity/postee/tree/main/rego-templates) ### Customizing Templates It 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) ### Troubleshooting of Rego Templates Rego templates provide very flexible way for transformation of received json. You can convert received information to html or json. On the flip side sometimes it may be difficult to find root cause of issue (if you run into any while configuring custom template). Postee 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: ``` 2021/07/23 18:27:31 Error while evaluating input: property result is not found ``` So here are details to help with troubleshooting: #### Required tools - [opa](https://www.openpolicyagent.org/docs/latest/#running-opa) - tool to evaluate OPA queries directly - [jq](https://stedolan.github.io/jq/) - flexible command-line JSON processor. #### Evaluate template to build html Here is example of command to evaluate rego: ``` opa eval data.postee.vuls.html.result --data vuls-html.rego --data common/common.rego --input | jq -r .result[0].expressions[0].value ``` The 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`.``.`result`. You may want to evaluate title property. In this case query would be: `data`.``.`title` #### Evaluate template to build json ``` cd rego-templates opa eval data.postee.vuls.slack.result --data vuls-slack.rego --data common/common.rego --input | jq .result[0].expressions[0].value ``` The command above is similar to html case but `jq` is used a bit different way. ================================================ FILE: docs/troubleshooting-of-rego-templates.md ================================================ ================================================ FILE: docs/ui.md ================================================ Postee provides a simple Web UI to simplify the configuration management. ![Config app](img/postee-output-config.png) ## Configure and run Postee UI application ### Requirements Postee 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. !!! danger 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. ### Kubernetes for Postee UI application The manifest is [here](https://github.com/aquasecurity/postee/blob/main/deploy/kubernetes/postee.yaml). It will expose a service `postee-ui-svc` in the port `8000`. `http://postee-ui.default.svc.cluster.local:8000` ### Docker Image for Postee UI application Dockerfile to build image for UI app is [here](Dockerfile.ui) ### Orchestration example (Docker Compose) There 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`. ### Environment variables Name | Description | Default value --- | --- | --- POSTEE_UI_CFG|Path to app config| required, no default value POSTEE_UI_PORT|Port to use with UI app| 8090 POSTEE_UI_UPDATE_URL|Url of webhook application|required POSTEE_ADMIN_USER|Admin account name|admin POSTEE_ADMIN_PASSWORD|Admin account password|admin ================================================ FILE: formatting/eval.go ================================================ package formatting import ( "bytes" "encoding/json" "errors" "fmt" "net/url" "strings" "github.com/aquasecurity/postee/v2/data" "github.com/aquasecurity/postee/v2/layout" ) type legacyScnEvaluator struct { layoutProvider layout.LayoutProvider } func (legacyScnEvaluator *legacyScnEvaluator) Eval(in map[string]interface{}, serverUrl string) (map[string]string, error) { scan, err := toScanImage(in) if err != nil { return nil, err } title := fmt.Sprintf("%s vulnerability scan report", in["image"]) image_url_part := scan.Registry + "/" + url.QueryEscape(scan.Image) return map[string]string{ "title": title, "description": layout.GenTicketDescription(legacyScnEvaluator.layoutProvider, scan, nil, serverUrl, image_url_part), "url": serverUrl + image_url_part, }, nil } func (legacyScnEvaluator *legacyScnEvaluator) IsAggregationSupported() bool { return true } func (legacyScnEvaluator *legacyScnEvaluator) BuildAggregatedContent(scans []map[string]string) (map[string]string, error) { var descr bytes.Buffer var urls bytes.Buffer owners := []string{} for _, scan := range scans { descr.WriteString(legacyScnEvaluator.layoutProvider.TitleH1(scan["title"])) descr.WriteString(scan["description"]) if urls.Len() > 0 { urls.WriteByte('\n') } urls.WriteString(scan["url"]) if len(scan["owners"]) > 0 { owners = append(owners, scan["owners"]) } } title := "Vulnerability scan report" r := map[string]string{ "title": title, "description": descr.String(), "url": urls.String(), //TODO this is strange ... } if len(owners) > 0 { r["owners"] = strings.Join(owners, ";") } return r, nil } func toScanImage(in map[string]interface{}) (*data.ScanImageInfo, error) { source, err := json.Marshal(in) //back to bytes if err != nil { return nil, err } scanInfo := new(data.ScanImageInfo) err = json.Unmarshal(source, scanInfo) if err != nil { return nil, err } return scanInfo, nil } func BuildLegacyScnEvaluator(layoutType string) (data.Inpteval, error) { switch layoutType { case "slack": return &legacyScnEvaluator{ layoutProvider: &SlackMrkdwnProvider{}, }, nil case "html": return &legacyScnEvaluator{ layoutProvider: &HtmlProvider{}, }, nil case "jira": return &legacyScnEvaluator{ layoutProvider: &JiraLayoutProvider{}, }, nil default: return nil, errors.New("unknown layout type") } } ================================================ FILE: formatting/eval_test.go ================================================ package formatting import ( "encoding/json" "fmt" "strings" "testing" "github.com/stretchr/testify/assert" ) var ( scan1 = `{ "image":"Demo mock image1", "registry":"registry1", "digest":"abc", "vulnerability_summary":{"critical":0,"high":1,"medium":3,"low":4,"negligible":5}, "image_assurance_results":{"disallowed":true} }` ) func TestEval(t *testing.T) { expectedTitle := "Demo mock image1 vulnerability scan report" expectedDescription := `

Image name: Demo mock image1

Registry: registry1

Image is non-compliant

CRITICALHIGHMEDIUMLOWNEGLIGIBLE
01345
` in := map[string]interface{}{} if err := json.Unmarshal([]byte(scan1), &in); err != nil { t.Fatalf("json.Unmarshal error for %s: %v\n", scan1, err) } e, err := BuildLegacyScnEvaluator("html") if err != nil { t.Fatalf("Unexpected error %v\n", err) } out, err := e.Eval(in, "") assert.NoError(t, err) if out["title"] != expectedTitle { t.Errorf("Unexpected title value got %s, expected %s\n", out["title"], expectedTitle) } if out["description"] != expectedDescription { t.Errorf("Unexpected description value got %s, expected %s\n", out["description"], expectedDescription) } } func TestAggregationSupport(t *testing.T) { e := &legacyScnEvaluator{} if !e.IsAggregationSupported() { t.Errorf("Legacy Scan Evaluator should support aggregation by default\n") } } func TestBuildAggregatedContent(t *testing.T) { expectedTitle := "Vulnerability scan report" expectedDescription := `

title1

description1

title2

description2` expectedUrl := `url1 url2` expectedOwners := []string{"admin", "user"} e, err := BuildLegacyScnEvaluator("html") if err != nil { t.Fatalf("Unexpected error %v\n", err) } in := []map[string]string{ { "title": "title1", "description": "description1", "url": "url1", "owners": "admin", }, { "title": "title2", "description": "description2", "url": "url2", "owners": "user", }, } out, err := e.BuildAggregatedContent(in) if err != nil { t.Fatalf("Unexpected error %v\n", err) } if out["title"] != expectedTitle { t.Errorf("Unexpected title value got %s, expected %s\n", out["title"], expectedTitle) } if out["description"] != expectedDescription { t.Errorf("Unexpected description value got %s, expected %s\n", out["description"], expectedDescription) } if out["url"] != expectedUrl { t.Errorf("Unexpected description value got %s, expected %s\n", out["url"], expectedUrl) } actualOwners := strings.Split(out["owners"], ";") if len(actualOwners) == len(expectedOwners) { for _, own := range actualOwners { found := false for _, expOwn := range expectedOwners { if own == expOwn { found = true break } } if !found { t.Errorf("Unexpected owner: %s\n", own) } } } else { t.Errorf("Unexpected owners value got %s, expected %s\n", out["owners"], expectedOwners) } } func TestBuildLegacyScnEvaluator(t *testing.T) { tests := []struct { layoutType string expectedLayoutClass string shouldFail bool }{ {"html", "*formatting.HtmlProvider", false}, {"jira", "*formatting.JiraLayoutProvider", false}, {"slack", "*formatting.SlackMrkdwnProvider", false}, {"xml", "", true}, } for _, test := range tests { e, err := BuildLegacyScnEvaluator(test.layoutType) if err == nil && test.shouldFail { t.Fatalf("BuildLegacyScnEvaluator should fail for layout type %s but actually didn't return an error\n", test.layoutType) } else if err != nil && !test.shouldFail { t.Fatalf("Unexpected error %v\n", err) } if test.shouldFail { return } scnEvaluator, ok := e.(*legacyScnEvaluator) if !ok { t.Fatalf("Unexpected type of evaluator returned %T\n", e) } actualCls := fmt.Sprintf("%T", scnEvaluator.layoutProvider) if actualCls != test.expectedLayoutClass { t.Errorf("Invalid type of layout provider returned, expected %s, got %s\n", test.expectedLayoutClass, actualCls) } } } func TestToScanImage(t *testing.T) { var wrongProp map[bool]string inp := make(map[string]interface{}) inp["wrongProp"] = wrongProp _, err := toScanImage(inp) if err == nil { t.Errorf("Error is expected\n") } } ================================================ FILE: formatting/htmlprovider.go ================================================ package formatting import ( "bytes" "fmt" "strings" ) type HtmlProvider struct{} func (html *HtmlProvider) P(p string) string { return fmt.Sprintf("

%s

\n", p) } func (html *HtmlProvider) TitleH1(title string) string { return fmt.Sprintf("

%s

\n", title) } func (html *HtmlProvider) TitleH2(title string) string { return fmt.Sprintf("

%s

\n", title) } func (html *HtmlProvider) TitleH3(title string) string { return fmt.Sprintf("

%s

\n", title) } func (html *HtmlProvider) ColourText(text, color string) string { return fmt.Sprintf("%s", color, text) } func (html *HtmlProvider) Table(rows [][]string) string { table := make([]string, 0) table = append(table, "") for i, r := range rows { var tag string if i == 0 { tag = "TH" } else { tag = "TD" } table = append(table, "") var rowBuilder bytes.Buffer for _, field := range r { rowBuilder.WriteString(fmt.Sprintf("<%s style='padding: 5px;'>%s", tag, field, tag)) } table = append(table, rowBuilder.String()) table = append(table, "") } table = append(table, "
\n") return strings.Join(table, "\n") } func (html *HtmlProvider) A(url, title string) string { return fmt.Sprintf("%s", url, title) } ================================================ FILE: formatting/htmlprovider_test.go ================================================ package formatting import ( "testing" ) func TestHtmlProvider_Table(t *testing.T) { var tests = []tableTest{ { source: [][]string{ {"Header1", "Header2"}, {"Field1", "Field2"}, }, result: `
Header1Header2
Field1Field2
`, }, } tableTesting(tests, t, new(HtmlProvider)) } func TestHtmlProviderTags(t *testing.T) { tests := []tagsTest{ { "Lorem Ipsum", "red", "url", "Lorem Ipsum", "

Lorem Ipsum

\n", "

Lorem Ipsum

\n", "

Lorem Ipsum

\n", "

Lorem Ipsum

\n", "Lorem Ipsum", }, } tagsTesting(tests, t, new(HtmlProvider)) } ================================================ FILE: formatting/jiraprovider.go ================================================ package formatting import ( "bytes" "fmt" "strings" ) type JiraLayoutProvider struct{} func (jira *JiraLayoutProvider) P(p string) string { return fmt.Sprintf("%s\n", p) } func (jira *JiraLayoutProvider) TitleH1(title string) string { return fmt.Sprintf("h1. %s\n", title) } func (jira *JiraLayoutProvider) TitleH2(title string) string { return fmt.Sprintf("h2. %s\n", title) } func (jira *JiraLayoutProvider) TitleH3(title string) string { return fmt.Sprintf("h3. %s\n", title) } func (jira *JiraLayoutProvider) ColourText(text, color string) string { return fmt.Sprintf("{color:%s}%s{color}", color, text) } func (jira *JiraLayoutProvider) Table(rows [][]string) string { if len(rows) == 0 { return "" } var builder bytes.Buffer for i, row := range rows { if i == 0 { fmt.Fprintf(&builder, "||%s||\n", strings.Join(row, "||")) } else { fmt.Fprintf(&builder, "|%s|\n", strings.Join(row, "|")) } } builder.WriteString("\n") return builder.String() } func (jira *JiraLayoutProvider) A(url, title string) string { return fmt.Sprintf("[%s|%s]", title, url) } ================================================ FILE: formatting/jiraprovider_test.go ================================================ package formatting import "testing" func TestJiraLayoutProvider_Tags(t *testing.T) { tests := []tagsTest{ { "Lorem Ipsum", "red", "url", "{color:red}Lorem Ipsum{color}", "h1. Lorem Ipsum\n", "h2. Lorem Ipsum\n", "h3. Lorem Ipsum\n", "Lorem Ipsum\n", "[Lorem Ipsum|url]", }, } tagsTesting(tests, t, new(JiraLayoutProvider)) } func TestJiraLayoutProvider_Table(t *testing.T) { var tests = []tableTest{ { source: [][]string{ {"Header1", "Header2"}, {"Field1", "Field2"}, }, result: `||Header1||Header2|| |Field1|Field2| `, }, { source: nil, result: "", }, } tableTesting(tests, t, new(JiraLayoutProvider)) } ================================================ FILE: formatting/markup_test.go ================================================ package formatting import ( "testing" "github.com/aquasecurity/postee/v2/layout" ) type tagsTest struct { source string color string link string colourText, h1, h2, h3, p, a string } type tableTest struct { source [][]string result string } func tagsTesting(tests []tagsTest, t *testing.T, provider layout.LayoutProvider) { for _, test := range tests { if c := provider.ColourText(test.source, test.color); c != test.colourText { t.Errorf("Wrong colorur text\nWaited: %q\n Result: %q", test.colourText, c) } if h1 := provider.TitleH1(test.source); h1 != test.h1 { t.Errorf("Wrong H1 formatting for %q\nWaited: %q\n Result: %q", test.source, test.h1, h1) } if h2 := provider.TitleH2(test.source); h2 != test.h2 { t.Errorf("Wrong H2 formatting for %q\nWaited: %q\n Result: %q", test.source, test.h2, h2) } if h3 := provider.TitleH3(test.source); h3 != test.h3 { t.Errorf("Wrong H3 formatting for %q\nWaited: %q\n Result: %q", test.source, test.h3, h3) } if p := provider.P(test.source); p != test.p { t.Errorf("Wrong P formatting for %q\nWaited: %q\n Result: %q", test.source, test.p, p) } if a := provider.A(test.link, test.source); a != test.a { t.Errorf("Wrong P formatting for link %q (%q)\nWaited: %q\n Result: %q", test.link, test.source, test.a, a) } } } func tableTesting(tests []tableTest, t *testing.T, provider layout.LayoutProvider) { for _, test := range tests { if got := provider.Table(test.source); got != test.result { t.Errorf("Error: html.Table(test.Source)\nResult: %s\nWaited: %s\n", got, test.result) } } } ================================================ FILE: formatting/slackmrkdwnprovider.go ================================================ package formatting import ( "bytes" "encoding/json" "fmt" "log" "github.com/aquasecurity/postee/v2/data" ) func getMrkdwnText(text string) string { block := &data.SlackBlock{ TypeField: "section", TextField: &data.SlackTextBlock{ TypeField: "mrkdwn", TextField: text, }, } result, err := json.Marshal(block) if err != nil { log.Printf("SlackMrkdwnProvider Error: %v", err) return "" } result = append(result, ',') return string(result) } type SlackMrkdwnProvider struct{} func (mrkdwn *SlackMrkdwnProvider) TitleH1(title string) string { return getMrkdwnText(fmt.Sprintf("*%s*", title)) } func (mrkdwn *SlackMrkdwnProvider) TitleH2(title string) string { return getMrkdwnText(fmt.Sprintf("*%s*", title)) } func (mrkdwn *SlackMrkdwnProvider) TitleH3(title string) string { return mrkdwn.TitleH2(title) } func (mrkdwn *SlackMrkdwnProvider) ColourText(text, color string) string { return fmt.Sprintf("*%s*", text) } func (mrkdwn *SlackMrkdwnProvider) Table(rows [][]string) string { if len(rows) == 0 { return "" } var builder bytes.Buffer fields := &data.SlackBlock{ TypeField: "section", } if len(rows) == 2 && len(rows[0]) == 5 { fields.Fields = make([]data.SlackTextBlock, 2*len(rows[0])) for i, r := range rows { for j, f := range r { if i == 0 { fields.Fields[j*2] = data.SlackTextBlock{ TypeField: "mrkdwn", TextField: fmt.Sprintf("*%s*", f), } } else { fields.Fields[j*2+1] = data.SlackTextBlock{ TypeField: "mrkdwn", TextField: f, } } } } } else { totalRows := len(rows) for line, r := range rows { if line%5 == 0 { if fields.Fields != nil { block, err := json.Marshal(fields) if err != nil { log.Printf("SlackMrkdwnProvider Error: %v", err) return "" } builder.Write(block) builder.WriteByte(',') } fields = new(data.SlackBlock) fields.TypeField = "section" current := 5 if (totalRows - line) < 5 { current = totalRows - line } fields.Fields = make([]data.SlackTextBlock, current*2) } var cell1, cell2 bytes.Buffer for j, f := range r { bold := "" if line == 0 { bold = "*" } switch j { case 0: fmt.Fprintf(&cell1, "%s%s%s", bold, f, bold) case 1: if rows[0][0] == "#" { fmt.Fprintf(&cell1, " %s%s%s", bold, f, bold) } else { fmt.Fprintf(&cell2, "%s%s%s", bold, f, bold) if len(r) > 2 { fmt.Fprint(&cell2, " / ") } } default: if j > 2 { cell2.WriteString(" / ") } fmt.Fprintf(&cell2, "%s%s%s", bold, f, bold) } } fields.Fields[(line%5)*2] = data.SlackTextBlock{ TypeField: "mrkdwn", TextField: cell1.String(), } fields.Fields[(line%5)*2+1] = data.SlackTextBlock{ TypeField: "mrkdwn", TextField: cell2.String(), } } } result, err := json.Marshal(fields) if err != nil { log.Printf("SlackMrkdwnProvider Error: %v", err) return "" } builder.Write(result) builder.WriteByte(',') return builder.String() } func (mrkdwn *SlackMrkdwnProvider) P(p string) string { return getMrkdwnText(p) } func (mrkdwn *SlackMrkdwnProvider) A(url, title string) string { return fmt.Sprintf("<%s|%s>", url, title) } ================================================ FILE: formatting/slackmrkdwnprovider_test.go ================================================ package formatting import "testing" func TestSlackMrkdwn(t *testing.T) { tests := []tagsTest{ { "Lorem Ipsum", "red", "url", "*Lorem Ipsum*", "{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"*Lorem Ipsum*\"}},", "{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"*Lorem Ipsum*\"}},", "{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"*Lorem Ipsum*\"}},", "{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"Lorem Ipsum\"}},", "", }, } tagsTesting(tests, t, new(SlackMrkdwnProvider)) } func TestSlackMrkdwnProvider_Table(t *testing.T) { var tests = []tableTest{ { source: [][]string{ {"Header1", "Header2"}, {"Field1", "Field2"}, }, result: `{"type":"section","fields":[{"type":"mrkdwn","text":"*Header1*"},{"type":"mrkdwn","text":"*Header2*"},{"type":"mrkdwn","text":"Field1"},{"type":"mrkdwn","text":"Field2"}]},`, }, { source: [][]string{ {"Header1", "Header2", "Header3"}, {"Field1", "Field2", "Field3"}, }, result: `{"type":"section","fields":[{"type":"mrkdwn","text":"*Header1*"},{"type":"mrkdwn","text":"*Header2* / *Header3*"},{"type":"mrkdwn","text":"Field1"},{"type":"mrkdwn","text":"Field2 / Field3"}]},`, }, { source: [][]string{ {"Critical", "High", "Medium", "Low", "Negligible"}, {"Field10", "Field5", "Field3", "F27", "F232"}, }, result: `{"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"}]},`, }, } tableTesting(tests, t, new(SlackMrkdwnProvider)) } ================================================ FILE: go.mod ================================================ module github.com/aquasecurity/postee/v2 go 1.18 require ( github.com/DependencyTrack/client-go v0.11.0 github.com/PagerDuty/go-pagerduty v1.5.1 github.com/aquasecurity/go-jira v0.0.0-20230705211506-0cd878ce5449 github.com/aws/aws-sdk-go-v2 v1.16.11 github.com/aws/aws-sdk-go-v2/config v1.17.1 github.com/aws/aws-sdk-go-v2/service/securityhub v1.22.7 github.com/aws/smithy-go v1.12.1 github.com/docker/docker v20.10.24+incompatible github.com/ghodss/yaml v1.0.0 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 github.com/nats-io/nats-server/v2 v2.7.4 github.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d github.com/nats-io/nkeys v0.3.0 github.com/open-policy-agent/opa v0.45.0 github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10 github.com/spf13/cobra v1.5.0 github.com/stretchr/testify v1.8.2 github.com/tidwall/gjson v1.14.0 go.etcd.io/bbolt v1.3.6 k8s.io/api v0.23.3 k8s.io/apimachinery v0.23.3 k8s.io/client-go v0.23.3 ) require ( github.com/Microsoft/go-winio v0.5.1 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/aws/aws-sdk-go-v2/credentials v1.12.14 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect github.com/fatih/structs v1.1.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.5.3 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.14.4 // indirect github.com/minio/highwayhash v1.0.2 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/onsi/ginkgo v1.16.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/trivago/tgo v1.0.7 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/term v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.1.0 // indirect k8s.io/klog/v2 v2.30.0 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) replace ( github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017 => github.com/docker/cli v20.10.9+incompatible github.com/satori/go.uuid v1.2.0 => github.com/satori/go.uuid v1.2.1-0.20181016170032-d91630c85102 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 => golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DependencyTrack/client-go v0.11.0 h1:1g+eHC8nJyIzi68zcs+dr3OHRvS1aC+4Uy3YKA0JJhc= github.com/DependencyTrack/client-go v0.11.0/go.mod h1:XLZnOksOs56Svq+K4xmBkN8U97gpP7r1BkhCc/xA8Iw= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/PagerDuty/go-pagerduty v1.5.1 h1:zpMQ8WwWlUahipB2q+ERVIA9D0/ti8kvsQUSagCK86g= github.com/PagerDuty/go-pagerduty v1.5.1/go.mod h1:txr8VbObXdk2RkqF+C2an4qWssdGY99fK26XYUDjh+4= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/aquasecurity/go-jira v0.0.0-20230705211506-0cd878ce5449 h1:iiLF0O6h/Y5bdWSmxlb2EhdozYa5HTn+asKHSqr0R0M= github.com/aquasecurity/go-jira v0.0.0-20230705211506-0cd878ce5449/go.mod h1:IHtKzIAdk0t3Xse7rJSY7pJlA8gB7lqY2b4l5WYZYsk= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go-v2 v1.16.11 h1:xM1ZPSvty3xVmdxiGr7ay/wlqv+MWhH0rMlyLdbC0YQ= github.com/aws/aws-sdk-go-v2 v1.16.11/go.mod h1:WTACcleLz6VZTp7fak4EO5b9Q4foxbn+8PIz3PmyKlo= github.com/aws/aws-sdk-go-v2/config v1.17.1 h1:BWxTjokU/69BZ4DnLrZco6OvBDii6ToEdfBL/y5I1nA= github.com/aws/aws-sdk-go-v2/config v1.17.1/go.mod h1:uOxDHjBemNTF2Zos+fgG0NNfE86wn1OAHDTGxjMEYi0= github.com/aws/aws-sdk-go-v2/credentials v1.12.14 h1:AtVG/amkjbDBfnPr/tuW2IG18HGNznP6L12Dx0rLz+Q= github.com/aws/aws-sdk-go-v2/credentials v1.12.14/go.mod h1:opAndTyq+YN7IpVG57z2CeNuXSQMqTYxGGlYH0m0RMY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12 h1:wgJBHO58Pc1V1QAnzdVM3JK3WbE/6eUF0JxCZ+/izz0= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.12/go.mod h1:aZ4vZnyUuxedC7eD4JyEHpGnCz+O2sHQEx3VvAwklSE= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18 h1:OmiwoVyLKEqqD5GvB683dbSqxiOfvx4U2lDZhG2Esc4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.18/go.mod h1:348MLhzV1GSlZSMusdwQpXKbhD7X2gbI/TxwAPKkYZQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12 h1:5mvQDtNWtI6H56+E4LUnLWEmATMB7oEh+Z9RurtIuC0= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.12/go.mod h1:ckaCVTEdGAxO6KwTGzgskxR1xM+iJW4lxMyDFVda2Fc= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19 h1:g5qq9sgtEzt2szMaDqQO6fqKe026T6dHTFJp5NsPzkQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.19/go.mod h1:cVHo8KTuHjShb9V8/VjH3S/8+xPu16qx8fdGwmotJhE= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12 h1:7iPTTX4SAI2U2VOogD7/gmHlsgnYSgoNHt7MSQXtG2M= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.12/go.mod h1:1TODGhheLWjpQWSuhYuAUWYTCKwEjx2iblIFKDHjeTc= github.com/aws/aws-sdk-go-v2/service/securityhub v1.22.7 h1:NXJ6NdzJvXkUSwmwbSRtWPvTfIg5BueQ2Z1vid8o9CQ= github.com/aws/aws-sdk-go-v2/service/securityhub v1.22.7/go.mod h1:byhebHID81uPiHS2NQcZrKxOiB2roj3OOcWMvdxxjmk= github.com/aws/aws-sdk-go-v2/service/sso v1.11.17 h1:pXxu9u2z1UqSbjO9YA8kmFJBhFc1EVTDaf7A+S+Ivq8= github.com/aws/aws-sdk-go-v2/service/sso v1.11.17/go.mod h1:mS5xqLZc/6kc06IpXn5vRxdLaED+jEuaSRv5BxtnsiY= github.com/aws/aws-sdk-go-v2/service/sts v1.16.13 h1:dl8T0PJlN92rvEGOEUiD0+YPYdPEaCZK0TqHukvSfII= github.com/aws/aws-sdk-go-v2/service/sts v1.16.13/go.mod h1:Ru3QVMLygVs/07UQ3YDur1AQZZp2tUNje8wfloFttC0= github.com/aws/smithy-go v1.12.1 h1:yQRC55aXN/y1W10HgwHle01DRuV9Dpf31iGkotjt3Ag= github.com/aws/smithy-go v1.12.1/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bytecodealliance/wasmtime-go v1.0.0 h1:9u9gqaUiaJeN5IoD1L7egD8atOnTGyJcNp8BhkL9cUU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-retryablehttp v0.5.1/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4= github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY= github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= github.com/nats-io/nats-server/v2 v2.7.4 h1:c+BZJ3rGzUKCBIM4IXO8uNT2u1vajGbD1kPA6wqCEaM= github.com/nats-io/nats-server/v2 v2.7.4/go.mod h1:1vZ2Nijh8tcyNe8BDVyTviCd9NYzRbubQYiEHsvOQWc= github.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d h1:zJf4l8Kp67RIZhoVeniSLZs69SHNgjLHz0aNsqPPlx8= github.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/open-policy-agent/opa v0.45.0 h1:P5nuhVRtR+e58fk3CMMbiqr6ZFyWQPNOC3otsorGsFs= github.com/open-policy-agent/opa v0.45.0/go.mod h1:/OnsYljNEWJ6DXeFOOnoGn8CvwZGMUS4iRqzYdJvmBI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10 h1:qHnitdkr8TN/irubnQM8ml/udTyAxo6j5v61H7+TV3k= github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10/go.mod h1:4OjcxgwdXzezqytxN534MooNmrxRD50geWZxTD7845s= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= gotest.tools/v3 v3.1.0/go.mod h1:fHy7eyTmJFO5bQbUsEGQ1v4m2J3Jz9eWL54TP2/ZuYQ= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.23.3 h1:KNrME8KHGr12Ozjf8ytOewKzZh6hl/hHUZeHddT3a38= k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ= k8s.io/apimachinery v0.23.3 h1:7IW6jxNzrXTsP0c8yXz2E5Yx/WTzVPTsHIx/2Vm0cIk= k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/client-go v0.23.3 h1:23QYUmCQ/W6hW78xIwm3XqZrrKZM+LWDqW2zfo+szJs= k8s.io/client-go v0.23.3/go.mod h1:47oMd+YvAOqZM7pcQ6neJtBiFH7alOyfunYN48VsmwE= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= ================================================ FILE: integration/controller_runner_test.go ================================================ //go:build integration package integration import ( "io/ioutil" "os" "testing" "github.com/aquasecurity/postee/v2/controller" "github.com/aquasecurity/postee/v2/router" "github.com/aquasecurity/postee/v2/runner" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) const ( RunnerConfig = ` routes: - name: terminate-and-notify input: contains(input.SigMetadata.ID, "TRC-2") actions: [terminate-pod] plugins: {} actions: - name: terminate-pod runs-on: "test-runner-1" type: exec enable: true exec-script: | #!/bin/sh PID=$(echo $POSTEE_EVENT | jq -r .Context.hostName) kubectl delete pod $PID # If terminating a K8s pod # pkill -SIGTERM $PID # If terminating a UNIX process ` ) func TestControllerRunner_Happy(t *testing.T) { testCases := []struct { name string cCfg controller.Controller rCfg runner.Runner expectedConfig string }{ { name: "no tls, no auth", cCfg: controller.Controller{ ControllerURL: "nats://0.0.0.0:17777", RunnerName: "test-runner-1", }, rCfg: runner.Runner{ ControllerURL: "nats://0.0.0.0:17777", RunnerName: "test-runner-1", }, expectedConfig: RunnerConfig, }, { name: "with tls, no auth", cCfg: controller.Controller{ ControllerURL: "tls://0.0.0.0:18888", RunnerName: "test-runner-1", ControllerTLSKeyPath: "goldens/server-key.pem", ControllerTLSCertPath: "goldens/server-cert.pem", ControllerCAFile: "goldens/rootCA.pem", }, rCfg: runner.Runner{ ControllerURL: "tls://0.0.0.0:18888", RunnerName: "test-runner-1", RunnerCARootPath: "goldens/rootCA.pem", RunnerTLSCertPath: "goldens/client-cert.pem", RunnerTLSKeyPath: "goldens/client-key.pem", }, expectedConfig: RunnerConfig, }, { name: "with tls, with auth", cCfg: controller.Controller{ ControllerURL: "tls://0.0.0.0:19999", RunnerName: "test-runner-1", ControllerTLSKeyPath: "goldens/server-key.pem", ControllerTLSCertPath: "goldens/server-cert.pem", ControllerCAFile: "goldens/rootCA.pem", ControllerSeedFilePath: "goldens/test-seed.txt", }, rCfg: runner.Runner{ ControllerURL: "tls://0.0.0.0:19999", RunnerName: "test-runner-1", RunnerCARootPath: "goldens/rootCA.pem", RunnerTLSCertPath: "goldens/client-cert.pem", RunnerTLSKeyPath: "goldens/client-key.pem", RunnerSeedFilePath: "goldens/test-seed.txt", }, expectedConfig: RunnerConfig, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { rtr := router.Instance() require.NoError(t, tc.cCfg.Setup(rtr)) require.NoError(t, rtr.Start("goldens/simple.yaml")) f, err := ioutil.TempFile("", "TestRunner_Setup-*") defer func() { os.Remove(f.Name()) }() require.NoError(t, err) require.NoError(t, tc.rCfg.Setup(rtr, f)) got, err := ioutil.ReadFile(f.Name()) require.NoError(t, err) assert.YAMLEq(t, tc.expectedConfig, string(got)) rtr.Terminate() }) } } ================================================ FILE: integration/goldens/client-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIEUjCCArqgAwIBAgIRAPLnJ75aAxz0TfngEX3vEikwDQYJKoZIhvcNAQELBQAw ZzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMR4wHAYDVQQLDBV2YWdy YW50QHVidW50dS1pbXBpc2gxJTAjBgNVBAMMHG1rY2VydCB2YWdyYW50QHVidW50 dS1pbXBpc2gwHhcNMjIwNTE3MjAxNjU1WhcNMjQwODE3MjAxNjU1WjBJMScwJQYD VQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNhdGUxHjAcBgNVBAsMFXZh Z3JhbnRAdWJ1bnR1LWltcGlzaDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBALlS5IVD77EWWok1OBzw/k7LNvkEsykSVfU9Y0KvbpR8nOqt877j/PHohFIr 5LdMFjeNWqgwAXhpPcjUKXd7irYZ4yEJJRyGYD4VuByCVq9pPd72nbf3ViK6V890 0+JmrNXyFVLYgYuOXuzYgGa3hyx6qRvcTTacHKfwbGCn05ZnFIX58+Pe+cRoXLAc 10iytPPxtlu6r7bV0OYd3B3RcH2lW/eufNijYGlpGwtAatsN7RkUpW/PUOqtDVFt OhmxMeXGJkPZpe8rgXPhzSVsCv8YvPISpj16tGCIBADDInyVneMAS6mfMzuOXn7L hjSPFd87KS1PvhWGJlKqVqszd4cCAwEAAaOBljCBkzAOBgNVHQ8BAf8EBAMCBaAw JwYDVR0lBCAwHgYIKwYBBQUHAwIGCCsGAQUFBwMBBggrBgEFBQcDBDAfBgNVHSME GDAWgBQbhjMiZMT6ZfLuxSV+BFqynUBfTDA3BgNVHREEMDAugglsb2NhbGhvc3SB D2VtYWlsQGxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsF AAOCAYEARan69HAf3nbwF5gqn11F3J/Li2rW3KHcDKy8fVzCvWajoXfAW3VMfqIO o5FZ3xYHXENvpY3AclxZgNZrN7B/uOac4iiI0c+NcyzaztOtgD72VDalHl9Cp6N9 xp/bF48OPDY9f33+fRieXRt8pGKzRlb9xsuvKJyCQvXrEw9S21eg7LrNk4F9pCQa e9PL1HOygvpltH/9h/jqL4lnWp/8GBHMbEAllk8gLCHPMFl5LSGrOoqfJDOxPphZ kuduHby0KhlEUejiV0mG0MIof4uagZTm7mQ7nguvxNu6wW6gU8Pwp6Lw//7bwkWc L4NtlHk4NhS9yB7uL/STVC2ZMH2Nfy2z9j0jpIjZ4kyvV6Zd4j9dVbvNzD/op6Y9 KNGq/g3aYqPHrn4MZ/1FiQBY2YtEHSSBbc2YJxBjL1fZJ0gGYVYB5o0qEepq41w4 QYprUb+TrCQ0NX8i9iJla2s2CXwLUdRFObvlp9Q5SswMuWjYXNNNrk6UryKRnc7d pE9OyCLq -----END CERTIFICATE----- ================================================ FILE: integration/goldens/client-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5UuSFQ++xFlqJ NTgc8P5Oyzb5BLMpElX1PWNCr26UfJzqrfO+4/zx6IRSK+S3TBY3jVqoMAF4aT3I 1Cl3e4q2GeMhCSUchmA+FbgcglavaT3e9p2391YiulfPdNPiZqzV8hVS2IGLjl7s 2IBmt4cseqkb3E02nByn8Gxgp9OWZxSF+fPj3vnEaFywHNdIsrTz8bZbuq+21dDm Hdwd0XB9pVv3rnzYo2BpaRsLQGrbDe0ZFKVvz1DqrQ1RbToZsTHlxiZD2aXvK4Fz 4c0lbAr/GLzyEqY9erRgiAQAwyJ8lZ3jAEupnzM7jl5+y4Y0jxXfOyktT74VhiZS qlarM3eHAgMBAAECggEAUz29cXKDbjqJLgoeizlgMQosRgvrYW5f98i8Fg7wj3et j+fUjRCg5BO5o6Mdl4twi0FN0C/H6Ur0Oom5mnZ+Zq8YCxWterOM9TYSPZP51c5i wQxq6WAKa4zhtkhWh+HwjiSvmUewYHvxfVb9zaVv7PWnkeEROY9De9eNBpk7e9lK INhXH06h73snqOWfQOa3vvGnvQY2d1FikIl9xVgbB2JyiyMWnQqWOiua4ES1qGvg IW2ZdrxkMOltVZVIFcyEN1N+Yq7uSOS2AdcJvgS5tSDEQGYsk1myPvh40rbSwE65 Mu8CEmBtiahPj2yURtjr1QJRMwfpqKMEA1vs+rkPKQKBgQDBlZVOa79YpjFypb/E ZuyWOzxbQPe4vuN4Xg6/L22SQQyQcPXOmudOq5PInzyltUS/O2or69JO0elc2e4O bi9p0qcTAFRFwS3MXPKoWhJx7qz6kBaAngvsvazBoEPbVTul9jf5N4yZ1PIKWvZ1 cNsbW69CDFcJa0FhpqdEZadxqwKBgQD1E30BcmdCuY0hZvY8Ys4AjCcOo39n3v9i CZfu10sNu6G6HFcA1vl/0w3mt2Zhe/AVKkI4tMBFrwAcWOWgnJHcfttMBA0IhSOb lifJtNAtdpD8YyESm4sDJkN9NRiIcjl5SfbQTqfv2jTUS2IHwErSpA2IGUPxMmpv ndQW+IntlQKBgQCWhghNrQxhEwEaA3XEcr995Vt+HVtBxPQ88O4IjQlnEruBSMRp PRukmVdVRTQ0KWnmRH2+3yRtc11AfJhVkim94DVXWgctIhPJd3CbpUX4Xz0Tq5xD sCDXmXjOKh75WiICXxyQ5TenmeQGV0qScQl+EWGKOuwmf8ab9qjTTzPO9wKBgDtv jIOLUUu3YGjXZaRjef5b/yTntgBk7p7CINzFzef4t1JBiUAk4sGDV/26QNQnkKQx aXoDLBY6SNaXIKmBD0bgZSLcYKTOVUJoeiHK/inodWUgTWcL3fzXjMqS0+5TdWja 7Ua7rLLYwrMxzQaANiU7sCRvrDCUv4duSX+Aq0K5AoGBAL5TWCYZy4tqzybQ4SJ4 9ZkK/ifDBlsjvAzaAfQ+I0ezZGpwAWhp0cRQEnso+fTEAE9cbBwrapejmy0HRCEn u1ah8an+C855EZ/3/VLVlFru5oqTpFwcEf/UZjux3tipPlNCoGCvdSr7HKaIWeu+ UNYIGo5P6qVxBov76JbEErvD -----END PRIVATE KEY----- ================================================ FILE: integration/goldens/rootCA.pem ================================================ -----BEGIN CERTIFICATE----- MIIEnjCCAwagAwIBAgIRAOPa8EEi4WZW/wxKT9ad82AwDQYJKoZIhvcNAQELBQAw ZzEeMBwGA1UEChMVbWtjZXJ0IGRldmVsb3BtZW50IENBMR4wHAYDVQQLDBV2YWdy YW50QHVidW50dS1pbXBpc2gxJTAjBgNVBAMMHG1rY2VydCB2YWdyYW50QHVidW50 dS1pbXBpc2gwHhcNMjIwNTE3MjAxNjQwWhcNMzIwNTE3MjAxNjQwWjBnMR4wHAYD VQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExHjAcBgNVBAsMFXZhZ3JhbnRAdWJ1 bnR1LWltcGlzaDElMCMGA1UEAwwcbWtjZXJ0IHZhZ3JhbnRAdWJ1bnR1LWltcGlz aDCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAPIb03qvQ7kn/1UzKLC/ wFYC8I7l5Aqfk3HyzFsOcWAh2NuyEhWq2TGhc88PXMIh1qtG6Np84BHbBdOjJNol 5QaNF/olelGykEEcdedPrtiH52rlgRdVlaeal/DbnzoNdJfArFPme0rzBDwSQkRi ND+mVjXObNKcH9tc8Jb3SOCQ0HHSrmltetZPgSCM1msXUJneVuJWeAr632OVXrZa 1AEdjf22xd1LePEvOvsmbHMvw/XgLDEMyLQmzdXGmwAhzgKzwGkvQYxvilqvJB31 xm+ws81mVpbOq/huMnxAMoZjvxOm5RdrJF1BZmrlNDtTGBlA0dhTa1xFvxuC9x19 SXoAzc66gn/wsDx3ipKqzyGgeoig6ZVAZtwofv9BozEE3cnZ7ovsvG+a9Qyvr4KH 2KLjjynV1sLRhhwGDawqaYXJRK/2MMaFZ29/UyuKMr4c5fYZncZAn5EgXvvUJaVe zG4SVw4mGciDIUfFcoYqbquEJA5XOORlK2y4zCR91yKg0wIDAQABo0UwQzAOBgNV HQ8BAf8EBAMCAgQwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUG4YzImTE +mXy7sUlfgRasp1AX0wwDQYJKoZIhvcNAQELBQADggGBAMwqGJMVD8FOsDh1Bfa5 dCZ/ybtUgPTcI4Pky98kilhlV1bPeacW6SD4W3Yf7sH1kVKSQgFS88nxLpcczGiw HO7gpgRv8QkoWGlIwdAlIvW/BAf8vByvpXBl1kd6anOAnwfwndxgUCc7aqZE/4xE vD0TJ+cbg+w81pmYwayouVnfHRpK5wKsGp2erEaGZceoZIfdHqNE3ecGV4r3izEC 3CgPTaLcGwkKkP18j5pOaL034nnxZV8uM0pwitHUMubMMAXiO86CayYzroKElTEa m6pHIOOBc/e3kkUdiJYLDeM4zlNIvZrdFSg2R6AKeIjMF1VOeW+5kvc0rmpbkeaI iIXoBkwzad/jab9XxhEqFK6/JL9VOH2Yf36F47sAu9vSRZbhWXNVUzvL7ktIpfHL 6COqoVOttkZj8yRyWje3M/RCnk6gS1BKxSDhO85kAy5zbNk1khgOO4W+ur4tFatv +iGSbAcOFDwb6Ygnp9+Y/ZhMARZHJfDg39/BhQIYQ1cCNw== -----END CERTIFICATE----- ================================================ FILE: integration/goldens/server-cert.pem ================================================ -----BEGIN CERTIFICATE----- MIIEHjCCAoagAwIBAgIQKa6srh/okxC7U4sW60pAvzANBgkqhkiG9w0BAQsFADBn MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExHjAcBgNVBAsMFXZhZ3Jh bnRAdWJ1bnR1LWltcGlzaDElMCMGA1UEAwwcbWtjZXJ0IHZhZ3JhbnRAdWJ1bnR1 LWltcGlzaDAeFw0yMjA1MTcyMTAzNDlaFw0yNDA4MTcyMTAzNDlaMEkxJzAlBgNV BAoTHm1rY2VydCBkZXZlbG9wbWVudCBjZXJ0aWZpY2F0ZTEeMBwGA1UECwwVdmFn cmFudEB1YnVudHUtaW1waXNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC AQEAmwLSqDxB+9/JipYDzUoE88aMo1B7O3fUgg7pOQS7m1YVwnB9YDrrisyd4Khk 5f3hUbf9tly1OjaVgDfs6khT2QVjs4fxXRYNF9+e1W53EyXokpqGybhrfkWHPdYw ePQrx6wn4+Fa5qm7BrIltWEKNMKAg0esTrPjnWR7MHZoJ4tAd00o0ymPxtXTAvXY qpc/nYDE8823jqhW182wu3zVwyZGBlvy9MrPMCA4X+3nT6nyPlAl77E87Nztahch +aiGn7K0Q+Yfqn7vf7p1g11syYThUHSqSRQUMrBObeMXp/9RErlbza5fm9VM30RG LD2beMvTkKDWd5/nxzvyMKtAKQIDAQABo2QwYjAOBgNVHQ8BAf8EBAMCBaAwEwYD VR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0jBBgwFoAUG4YzImTE+mXy7sUlfgRasp1A X0wwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwQAAAAAMA0GCSqGSIb3DQEBCwUAA4IB gQBqXvYt4m95MAA7Zy+0AJUCbJTUC5W8Kq2e5dvxCAETcgBsw2Aroob5tilhALgS 8JK3MnwUvyti+qyP8TmAVp4tu3VyZw0ME5B0ArTSHwuuvSZv59OYX85ioJLsGDuV TET2d6VMH5NpIe70DYtoib1NvzLcBxyR8olrz0Xc6tOwWwoxc8lPakdwBcVTPnye Ih2fBla+7h28CSniy3xVpcdum0zkKtlTp2Rf6chHAcS7SMrwSAjRccFDqCm+XhEA bSqJC7xyKxgJ6SBDoX02jFQXmsE0i0UoGQCKztB+JJZaxMPs9KojK3dBKdEUtAbO NTEg66a36wTogb7EtCv/qShFXsotm+xWXoCHFms/8/pbVrYbSVrrAPN4d6ctWMNT 6Pp5a+vBh45bHZaGRypspdK+cpJp8i90fxx/fccqh+y9CjlOIuaI86V2agUfPM3r 5K2Vt6A2fmmLH8iuUzfTeMc5+/VGrhXW3toMuYfVDe1t6owmHpc9PjQBxXsAKwS7 NEA= -----END CERTIFICATE----- ================================================ FILE: integration/goldens/server-key.pem ================================================ -----BEGIN PRIVATE KEY----- MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCbAtKoPEH738mK lgPNSgTzxoyjUHs7d9SCDuk5BLubVhXCcH1gOuuKzJ3gqGTl/eFRt/22XLU6NpWA N+zqSFPZBWOzh/FdFg0X357VbncTJeiSmobJuGt+RYc91jB49CvHrCfj4VrmqbsG siW1YQo0woCDR6xOs+OdZHswdmgni0B3TSjTKY/G1dMC9diqlz+dgMTzzbeOqFbX zbC7fNXDJkYGW/L0ys8wIDhf7edPqfI+UCXvsTzs3O1qFyH5qIafsrRD5h+qfu9/ unWDXWzJhOFQdKpJFBQysE5t4xen/1ESuVvNrl+b1UzfREYsPZt4y9OQoNZ3n+fH O/Iwq0ApAgMBAAECggEAAXsLbBP3RWKj2DEKpEFavsFcwoqYQr8oRn3B+It1B0zR NGRCrlKhwVenAmhZXmEIb/Fj+QP1rzKqRulB2BMMyfvlMefQhGUbhujZ6Yv+ky4/ hHb5Zp/eVvxvCEYtfFpIuMWKNAEJDz77zFqTRaJBqLNa6uYaXT4WpIA7NQFMiWQl USFGqTyv/v/xWrPEf59vgglmlVBHHkn3iEyl//zlykSBojV9mGNVLwGT6pQzmyA+ U1dwb+f6VXWPT6X0d18EybjgNbS7KDh08o2m3TdHIn+GroP9Fz0y4XnnML6B7jOr HSoiXVmd53xc0lnvN+ZGVgbxOidqW6ZzKs74g24XyQKBgQDKanyjmX50QY54fVk1 b31Cgfy8TK7kn3ZxKie5GMbrjB1IjKgkBcaDH0oUICNquAKQuZf1vvnilIkg1wMz B7zYRIioNOvYTUKNI8RRy+opT9/GjE0pCnOmCVS6JMWH+JJSJ+8lK8+EAcg2CDDc J6GqH0VFDgpMgFC0pTD/6aQYSwKBgQDEC8EQ3/d8KOHKTywYcEqZimCcZiek5pJ7 PldTzA5WhJ0RoVG9zr1Q1aMOGh6nZdL0FzfeqzrvbnQzIoJNRzEjAvgu/WrLeTO6 R0TlM7Yv7/gMBvKAvhlt4rLSLj+jBklJ0o+EivonNvom5/E9GhTk9ThYFLpRGIM1 49ObasRo2wKBgCqHmUyeka1HoPG8FKRfmogA2+OMkSrEPpSkcNg5VoJjXDvEIgf/ lqgySNRVtFtV8mQKOt2NlN+AqhdWVfvA4nLmY3hENlGqWhNhsnwlRjwL5k7HZ+b5 pTb7uuupkj9xgPz2Gw9KVz27Sh/gNLPtN9CQ5XMO8icExvBNzS/YHXxlAoGAANNb 2MpYrHkvmj6DHQc8CTF//qifeh5Od55nl77AQLUGRFC9m2MHgROlQCX0Axh2rvh6 D8IA2YlJi/2VNuYZ2t/kU65ViM3V3e7251A0cDc+gC53Tvt99+yAfEodkn4wSNNO L2y2aPveSdOoPG2jsOMnfZd4wWatBskS1xC4CwkCgYARzyoxf2O+WnNrkE6B7Uc1 hyV8bqO5TQNjHG/l4lur/71GWBguEzNwGyyI2HS9xeK+95/1Y/euDXyFUvBVWjFT SVt1d/TVzUv6WxFaEHIhIjmPFhbzBNwwz9ixIOf3uTeifptaw+tjhLCwVQhMErgf Dj8U2KJri5KKNn6wBSwU6w== -----END PRIVATE KEY----- ================================================ FILE: integration/goldens/simple.yaml ================================================ name: Test routes: - name: terminate-and-notify input: contains(input.SigMetadata.ID, "TRC-2") actions: [terminate-pod, send-slack-message] actions: - name: stdout type: stdout enable: true - name: terminate-pod runs-on: "test-runner-1" type: exec enable: true exec-script: | #!/bin/sh PID=$(echo $POSTEE_EVENT | jq -r .Context.hostName) kubectl delete pod $PID # If terminating a K8s pod # pkill -SIGTERM $PID # If terminating a UNIX process - name: send-slack-message type: slack enable: true url: https://hooks.slack.com/services/TAAAA/BBB/ ================================================ FILE: integration/goldens/test-seed.txt ================================================ SUAGAA3TNI36JHTD6GLFJRR6KZIY7YXS2ZISHQA4LPZZZG2D6KG5JPV7DM UBUQ63VFZEW3IS7RGQQZF5DIT2FTCMTZAAHFENK3G5M6ADRZ5WAJLAQN ================================================ FILE: layout/assurances.go ================================================ package layout import ( "strconv" "github.com/aquasecurity/postee/v2/data" ) func RenderAssurances(provider LayoutProvider, assuranceResults data.ImageAssuranceResults) string { var assurances [][]string assurances = append(assurances, []string{"#", "Control", "Policy Name", "Status"}) for i, ass := range assuranceResults.ChecksPerformed { var status string if ass.Failed { status = "FAIL" } else { status = "PASS" } assurances = append(assurances, []string{ strconv.Itoa(i + 1), ass.Control, ass.PolicyName, status, }) } return provider.Table(assurances) } ================================================ FILE: layout/colors.go ================================================ package layout func CriticalColor() string { return "#c00000" } func HighColor() string { return "#e0443d" } func MediumColor() string { return "#f79421" } func LowColor() string { return "#e1c930" } func NegligibleColor() string { return "green" } ================================================ FILE: layout/malware.go ================================================ package layout import ( "bytes" "strconv" "github.com/aquasecurity/postee/v2/data" ) func RenderMalware(malware []data.MalwareData, provider LayoutProvider, builder *bytes.Buffer) { var table [][]string table = append(table, []string{"#", "Malware", "Hash", "Path"}) for i, malware := range malware { table = append(table, []string{strconv.Itoa(i), malware.Malware, malware.Hash, malware.Path}) } builder.WriteString(provider.Table(table)) } ================================================ FILE: layout/provider.go ================================================ package layout type LayoutProvider interface { TitleH1(title string) string TitleH2(title string) string TitleH3(title string) string ColourText(text, color string) string Table(rows [][]string) string P(p string) string A(url, title string) string } ================================================ FILE: layout/sensitive.go ================================================ package layout import ( "bytes" "github.com/aquasecurity/postee/v2/data" ) func RenderSensitiveData(sensitive []data.SensitiveData, provider LayoutProvider, builder *bytes.Buffer) { var table [][]string table = append(table, []string{"File name", "Path", "Type", "Hash"}) for _, s := range sensitive { table = append(table, []string{s.Filename, s.Path, s.Type, s.Hash}) } builder.WriteString(provider.Table(table)) } ================================================ FILE: layout/ticketLayout.go ================================================ package layout import ( "bytes" "strconv" "github.com/aquasecurity/postee/v2/data" ) func GenTestDescription(provider LayoutProvider, raw string) string { var builder bytes.Buffer builder.WriteString(provider.P(raw)) return builder.String() } func GenTicketDescription(provider LayoutProvider, scanInfo, prevScan *data.ScanImageInfo, serverUrl, image_url_part string) string { var builder bytes.Buffer builder.WriteString(provider.P("Image name: " + scanInfo.Image)) builder.WriteString(provider.P("Registry: " + scanInfo.Registry)) if scanInfo.Disallowed { builder.WriteString(provider.P("Image is non-compliant")) } else { builder.WriteString(provider.P("Image is compliant")) } if scanInfo.ScanMalware { if scanInfo.Malware > 0 { builder.WriteString(provider.P("Malware found: Yes")) } else { builder.WriteString(provider.P("Malware found: No")) } } if scanInfo.ScanSensitiveData { if scanInfo.Sensitive > 0 { builder.WriteString(provider.P("Sensitive data found: Yes")) } else { builder.WriteString(provider.P("Sensitive data found: No")) } } builder.WriteString(VulnerabilitiesTable(provider, [2][]string{ {"CRITICAL", "HIGH", "MEDIUM", "LOW", "NEGLIGIBLE"}, {strconv.Itoa(scanInfo.Critical), strconv.Itoa(scanInfo.High), strconv.Itoa(scanInfo.Medium), strconv.Itoa(scanInfo.Low), strconv.Itoa(scanInfo.Negligible)}, })) // Rendering Assurances if len(scanInfo.ImageAssuranceResults.ChecksPerformed) > 0 { builder.WriteString(provider.TitleH2("Assurance controls")) builder.WriteString(RenderAssurances(provider, scanInfo.ImageAssuranceResults)) } // Rendering Found vulnerabilities if len(scanInfo.Resources) > 0 { builder.WriteString(provider.TitleH2("Found vulnerabilities")) RenderVulnerabilities(scanInfo.Resources, provider, &builder) } // Discovered vulnerabilities from last scan: if prevScan != nil && len(prevScan.Resources) > 0 { builder.WriteString("\n") builder.WriteString(provider.TitleH2("Discovered vulnerabilities from last scan")) RenderVulnerabilities(prevScan.Resources, provider, &builder) } if len(scanInfo.Malwares) > 0 { builder.WriteString("\n") builder.WriteString(provider.TitleH2("Malware")) RenderMalware(scanInfo.Malwares, provider, &builder) } if len(scanInfo.SensitiveData) > 0 { builder.WriteString("\n") builder.WriteString(provider.TitleH2("Sensitive Data")) RenderSensitiveData(scanInfo.SensitiveData, provider, &builder) } // Checked that the aqua-server name is not empty if len(serverUrl) > 0 && len(image_url_part) > 0 { builder.WriteString(provider.P("See more: " + provider.A(serverUrl+image_url_part, serverUrl+image_url_part))) } return builder.String() } ================================================ FILE: layout/vulnerabilities.go ================================================ package layout import ( "bytes" "strings" "github.com/aquasecurity/postee/v2/data" ) const empty = "none" func RenderVulnerabilities(resources []data.InfoResources, provider LayoutProvider, builder *bytes.Buffer) { rating := make(map[string][][]string) for _, r := range resources { var resourceName, installedVersion string if r.ResourceDetails.Name == "" { resourceName = empty } else { resourceName = r.ResourceDetails.Name } if r.ResourceDetails.Version == "" { installedVersion = empty } else { installedVersion = r.ResourceDetails.Version } for _, v := range r.Vulnerabilities { var vulnerabilityId, fixVersion string if v.Name == "" { vulnerabilityId = empty } else { vulnerabilityId = v.Name } if v.FixVersion == "" { fixVersion = empty } else { fixVersion = data.ClearField(v.FixVersion) } key := strings.ToLower(v.Severity) rating[key] = append(rating[key], []string{vulnerabilityId, resourceName, installedVersion, fixVersion}) } } order := [...]string{"critical", "high", "medium", "low", "negligible"} for _, title := range order { vulnerabilities, ok := rating[title] if !ok { continue } builder.WriteString(provider.TitleH3(strings.Title(title) + " severity vulnerabilities")) var table [][]string table = append(table, []string{"Vulnerability ID", "Resource name", "Installed version", "Fix version"}) table = append(table, vulnerabilities...) builder.WriteString(provider.Table(table)) } } func VulnerabilitiesTable(provider LayoutProvider, rows [2][]string) string { if len(rows) != 2 && len(rows[1]) != 5 { return "" } var table [][]string table = append(table, rows[0]) var r []string r = append(r, provider.ColourText(rows[1][0], CriticalColor())) r = append(r, provider.ColourText(rows[1][1], HighColor())) r = append(r, provider.ColourText(rows[1][2], MediumColor())) r = append(r, provider.ColourText(rows[1][3], LowColor())) r = append(r, provider.ColourText(rows[1][4], NegligibleColor())) table = append(table, r) return provider.Table(table) } ================================================ FILE: main.go ================================================ package main import ( "fmt" "io/ioutil" "log" "os" "os/signal" "runtime" "syscall" "github.com/aquasecurity/postee/v2/controller" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/router" "github.com/aquasecurity/postee/v2/runner" "github.com/aquasecurity/postee/v2/utils" "github.com/aquasecurity/postee/v2/webserver" "github.com/spf13/cobra" ) const ( URL = "0.0.0.0:8082" TLS = "0.0.0.0:8445" URL_USAGE = "The socket to bind to, specified using host:port." TLS_USAGE = "The TLS socket to bind to, specified using host:port." CFG_FILE = "/config/cfg.yaml" CFG_USAGE = "The alert configuration file." ) var ( url = "" tls = "" cfgfile = "" controllerMode = false controllerURL = "" controllerCARootPath = "" controllerTLSCertPath = "" controllerTLSKeyPath = "" controllerSeedFilePath = "" runnerSeedFilePath = "" runnerName = "" runnerCARootPath = "" runnerTLSCertPath = "" runnerTLSKeyPath = "" ) var rootCmd = &cobra.Command{ Use: "webhooksrv", Short: fmt.Sprintf("Aqua Container Security Webhook server\n"), Long: fmt.Sprintf("Aqua Container Security Webhook server\n"), } func init() { rootCmd.Flags().StringVar(&url, "url", URL, URL_USAGE) rootCmd.Flags().StringVar(&tls, "tls", TLS, TLS_USAGE) rootCmd.Flags().StringVar(&cfgfile, "cfgfile", CFG_FILE, CFG_USAGE) rootCmd.Flags().BoolVar(&controllerMode, "controller-mode", false, "run postee in controller mode") rootCmd.Flags().StringVar(&controllerURL, "controller-url", "", "postee controller URL") rootCmd.Flags().StringVar(&controllerCARootPath, "controller-ca-root", "", "postee controller ca root file") rootCmd.Flags().StringVar(&controllerTLSCertPath, "controller-tls-cert", "", "postee controller TLS cert file") rootCmd.Flags().StringVar(&controllerTLSKeyPath, "controller-tls-key", "", "postee controller TLS key file") rootCmd.Flags().StringVar(&controllerSeedFilePath, "controller-seed-file", "", "postee controller AuthN seed file") rootCmd.Flags().StringVar(&runnerName, "runner-name", "", "postee runner name") rootCmd.Flags().StringVar(&runnerCARootPath, "runner-ca-root", "", "postee runner ca root file") rootCmd.Flags().StringVar(&runnerTLSCertPath, "runner-tls-cert", "", "postee runner tls cert file") rootCmd.Flags().StringVar(&runnerTLSKeyPath, "runner-tls-key", "", "postee runner tls key file") rootCmd.Flags().StringVar(&runnerSeedFilePath, "runner-seed-file", "", "postee runner AuthN seed file") } func main() { runtime.GOMAXPROCS(runtime.NumCPU()) utils.InitDebug() rootCmd.Run = func(cmd *cobra.Command, args []string) { rtr := router.Instance() if runnerName != "" { if controllerMode { log.Fatal("postee cannot run as a controller when running in runner mode") } f, err := ioutil.TempFile("", "temp-postee-config-*") // TODO: Find a better way if err != nil { log.Fatal("Unable to create temp file for runner config on disk: ", err) } rnr := runner.Runner{ ControllerURL: controllerURL, RunnerSeedFilePath: runnerSeedFilePath, RunnerCARootPath: runnerCARootPath, RunnerTLSKeyPath: runnerTLSKeyPath, RunnerTLSCertPath: runnerTLSCertPath, RunnerName: runnerName, } if err := rnr.Setup(rtr, f); err != nil { log.Fatal("Failed to launch runner: ", err) } defer func() { os.Remove(f.Name()) }() cfgfile = f.Name() } if controllerMode { if runnerName != "" { log.Fatal("postee cannot run as a runner when running in controller mode") } ctr := controller.Controller{ ControllerURL: controllerURL, ControllerSeedFilePath: controllerSeedFilePath, ControllerCAFile: controllerCARootPath, ControllerTLSKeyPath: controllerTLSKeyPath, ControllerTLSCertPath: controllerTLSCertPath, RunnerName: runnerName, } if err := ctr.Setup(rtr); err != nil { log.Fatal("Failed to launch controller: ", err) } } if os.Getenv("AQUAALERT_URL") != "" { url = os.Getenv("AQUAALERT_URL") } if os.Getenv("POSTEE_HTTP") != "" { url = os.Getenv("POSTEE_HTTP") } if os.Getenv("AQUAALERT_TLS") != "" { tls = os.Getenv("AQUAALERT_TLS") } if os.Getenv("POSTEE_HTTPS") != "" { tls = os.Getenv("POSTEE_HTTPS") } if os.Getenv("AQUAALERT_CFG") != "" { cfgfile = os.Getenv("AQUAALERT_CFG") } if os.Getenv("POSTEE_CFG") != "" { cfgfile = os.Getenv("POSTEE_CFG") } if os.Getenv("PATH_TO_DB") != "" { dbservice.SetNewDbPathFromEnv() } err := rtr.Start(cfgfile) if err != nil { log.Printf("Can't start alert manager %v", err) return } defer rtr.Terminate() go webserver.Instance().Start(url, tls) defer webserver.Instance().Terminate() Daemonize() } err := rootCmd.Execute() if err != nil { log.Printf("Can't start command %v", err) return } } func Daemonize() { sigs := make(chan os.Signal, 1) done := make(chan bool, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) go func() { sig := <-sigs log.Println(sig) done <- true }() <-done } ================================================ FILE: mkdocs.yml ================================================ site_name: Postee site_url: https://aquasecurity.github.io/postee site_description: Integrate vulnerability scanning with a variety of systems. docs_dir: docs/ repo_name: Postee repo_url: https://github.com/aquasecurity/postee edit_uri: "" copyright: Copyright 2019-2022 Aqua Security Software Ltd. nav: - Introduction: index.md - Use cases: - Simple Examples: examples.md - Blueprints: - Trivy Scan: blueprints/trivy-vulnerability-scan.md - Trivy AWS Security Hub: blueprints/trivy-aws-security-hub.md - Trivy Operator: blueprints/trivy-operator.md - Pagerduty: blueprints/devops-pagerduty.md - Others: - External Healthcheck: blueprints/external-healthcheck.md - Image Processing: blueprints/image-processing.md - Installation: install.md - Configuration: - Config File: config.md - General: settings.md - Routes: routes.md - Templates: templates.md - Actions: actions.md - Controller/Runner: controller-runner.md - Demo: demo.md - Aqua Cloud: aquacloud.md - Postee UI: ui.md - Advanced: advanced.md theme: name: material custom_dir: overrides language: 'en' logo: img/postee.png palette: - media: "(prefers-color-scheme: light)" scheme: default toggle: icon: material/toggle-switch-off-outline name: Switch to dark mode - media: "(prefers-color-scheme: dark)" scheme: slate toggle: icon: material/toggle-switch name: Switch to light mode markdown_extensions: - pymdownx.highlight - pymdownx.details - pymdownx.superfences - admonition - attr_list - md_in_html - toc: permalink: true # All data defined under extra is automatically exposed as a variable and can # be used from the template. For example, {{ var.version }}. # # Requires pip install mike and pip install git+https://${GH_TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git extra: generator: false social: - icon: fontawesome/brands/github link: https://github.com/aquasecurity - icon: fontawesome/brands/slack link: https://slack.aquasec.com/ - icon: fontawesome/brands/youtube link: https://www.youtube.com/channel/UCZd5NF4XJRaU-yfextsY-pw - icon: fontawesome/brands/twitter link: https://twitter.com/AquaSecTeam - icon: fontawesome/brands/linkedin link: https://www.linkedin.com/company/aquasecteam/ version: provider: mike # Requires pip install mkdocs-macros-plugin plugins: - search - macros ================================================ FILE: msgservice/aggregatebytime_test.go ================================================ package msgservice import ( "log" "os" "testing" "github.com/aquasecurity/postee/v2/actions" "github.com/aquasecurity/postee/v2/data" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/routes" ) func TestAggregateByTimeout(t *testing.T) { const aggregationSeconds = 3 dbPathReal := dbservice.DbPath savedRunScheduler := RunScheduler schedulerInvctCnt := 0 defer func() { os.Remove(dbservice.DbPath) dbservice.ChangeDbPath(dbPathReal) RunScheduler = savedRunScheduler }() RunScheduler = func( route *routes.InputRoute, fnSend func(plg actions.Action, cnt map[string]string), fnAggregate func(outputName string, currentContent map[string]string, counts int, ignoreLength bool) []map[string]string, inpteval data.Inpteval, name *string, output actions.Action, ) { log.Printf("Mocked Scheduler is activated for route %q. Period: %d sec", route.Name, route.Plugins.AggregateTimeoutSeconds) route.StartScheduler() schedulerInvctCnt++ } dbservice.ChangeDbPath("test_webhooks.db") dbservice.DbPath = "test_webhooks.db" demoRoute := &routes.InputRoute{ Name: "demo-route1", Plugins: routes.Plugins{ AggregateTimeoutSeconds: aggregationSeconds, }, } demoEmailPlg := &DemoEmailAction{} demoInptEval := &DemoInptEval{} srvUrl := "" srv1 := new(MsgService) srv1.MsgHandling([]byte(mockScan1), demoEmailPlg, demoRoute, demoInptEval, &srvUrl) srv1.MsgHandling([]byte(mockScan2), demoEmailPlg, demoRoute, demoInptEval, &srvUrl) srv1.MsgHandling([]byte(mockScan3), demoEmailPlg, demoRoute, demoInptEval, &srvUrl) expectedSchedulerInvctCnt := 1 if schedulerInvctCnt != expectedSchedulerInvctCnt { t.Errorf("Unexpected plugin invocation count %d, expected %d \n", schedulerInvctCnt, expectedSchedulerInvctCnt) } demoRoute.StopScheduler() } ================================================ FILE: msgservice/aggregatescan_test.go ================================================ package msgservice import ( "os" "sync" "testing" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/routes" ) func TestAggregateIssuesPerTicket(t *testing.T) { tests := []struct { caseDesc string expectedSntCnt int expectedRenderCnt int expectedAggrRenderCnt int skipAggrSpprt bool }{ { caseDesc: "basic", expectedSntCnt: 1, expectedRenderCnt: 4, expectedAggrRenderCnt: 1, }, { caseDesc: "no aggregation supported", expectedSntCnt: 4, expectedRenderCnt: 4, expectedAggrRenderCnt: 0, skipAggrSpprt: true, }, } dbPathReal := dbservice.DbPath defer func() { os.Remove(dbservice.DbPath) dbservice.ChangeDbPath(dbPathReal) }() dbservice.ChangeDbPath("test_webhooks.db") for _, test := range tests { doAggregate(t, test.caseDesc, test.expectedSntCnt, test.expectedRenderCnt, test.expectedAggrRenderCnt, test.skipAggrSpprt) } } func doAggregate(t *testing.T, caseDesc string, expectedSntCnt int, expectedRenderCnt int, expectedAggrRenderCnt int, skipAggrSpprt bool) { demoEmailAction := &DemoEmailAction{ emailCounts: 0, } scans := []string{mockScan1, mockScan2, mockScan3, mockScan4} srvUrl := "" demoRoute := &routes.InputRoute{} demoRoute.Name = "demo-route" demoRoute.Plugins.AggregateMessageNumber = 3 demoInptEval := &DemoInptEval{ skipAggrSpprt: skipAggrSpprt, } demoEmailAction.wg = &sync.WaitGroup{} demoEmailAction.wg.Add(expectedSntCnt) for _, scan := range scans { srv := new(MsgService) srv.MsgHandling([]byte(scan), demoEmailAction, demoRoute, demoInptEval, &srvUrl) } demoEmailAction.wg.Wait() if demoEmailAction.getEmailsCount() != expectedSntCnt { t.Errorf("%s: The number of sent email doesn't match expected value. Sent: %d, expected: %d ", caseDesc, demoEmailAction.getEmailsCount(), expectedSntCnt) } if demoInptEval.renderCnt != expectedRenderCnt { t.Errorf("%s: The number of render procedure invocations doesn't match expected value. It's called %d times, expected: %d ", caseDesc, demoInptEval.renderCnt, expectedRenderCnt) } if demoInptEval.aggrCnt != expectedAggrRenderCnt { t.Errorf("%s: The number of aggregation procedure invocations doesn't match expected value. It's called %d times, expected: %d ", caseDesc, demoInptEval.aggrCnt, expectedAggrRenderCnt) } } ================================================ FILE: msgservice/applicationscopeowner_test.go ================================================ package msgservice import ( "os" "strings" "sync" "testing" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/routes" ) var ( scnWithOwners = `{ "image":"Demo mock image1", "registry":"registry1", "vulnerability_summary":{"critical":0,"high":1,"medium":3,"low":4,"negligible":5}, "image_assurance_results":{"disallowed":true}, "application_scope_owners": ["recipient1@aquasec.com", "recipient1@aquasec.com"] }` ) func TestApplicationScopeOwner(t *testing.T) { dbPathReal := dbservice.DbPath defer func() { os.Remove(dbservice.DbPath) dbservice.ChangeDbPath(dbPathReal) }() dbservice.ChangeDbPath("test_webhooks.db") demoEmailAction := &DemoEmailAction{ emailCounts: 0, } srvUrl := "" demoRoute := &routes.InputRoute{} demoRoute.Name = "demo-route" demoInptEval := &DemoInptEval{} demoEmailAction.wg = &sync.WaitGroup{} demoEmailAction.wg.Add(1) srv := new(MsgService) if srv.EvaluateRegoRule(demoRoute, []byte(scnWithOwners)) { srv.MsgHandling([]byte(scnWithOwners), demoEmailAction, demoRoute, demoInptEval, &srvUrl) } demoEmailAction.wg.Wait() if len(demoEmailAction.payloads) != 1 { t.Errorf("Action Send method isn't called as expected! Number of invocation expected %d, got: %d", 1, len(demoEmailAction.payloads)) } sent := demoEmailAction.payloads[0] ownersStr, ok := sent["owners"] if !ok { t.Errorf("Owners key is missed from output payload") } owners := strings.Split(ownersStr, ";") for _, own := range owners { if own != "recipient1@aquasec.com" && own != "recipient2@aquasec.com" { t.Errorf("Unexpected owner value: '%s'", own) } } } ================================================ FILE: msgservice/calculateexpired_test.go ================================================ package msgservice import ( "testing" "time" "github.com/stretchr/testify/assert" ) func TestCalculateExpired(t *testing.T) { timeouts := []int{0, 1, 2, 100} for _, timeout := range timeouts { r := calculateExpired(timeout) if timeout == 0 { assert.Nil(t, r) } else { n := time.Now() diff := r.Sub(n) assert.GreaterOrEqual(t, float64(timeout), diff.Seconds()) } } } ================================================ FILE: msgservice/getuniqueid_test.go ================================================ package msgservice import ( "encoding/json" "os" "path/filepath" "sync" "testing" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/routes" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( unique_scan1 = `{ "image":"Demo mock image1", "registry":"registry1", "digest":"abc", "vulnerability_summary":{"critical":0,"high":1,"medium":3,"low":4,"negligible":5}, "image_assurance_results":{"disallowed":true} }` unique_scan2 = `{ "image":"Demo mock image2", "registry":"registry2", "digest":"def", "vulnerability_summary":{"critical":0,"high":1,"medium":3,"low":4,"negligible":5}, "image_assurance_results":{"disallowed":true} }` non_unique_payload = `{ "action": "some", "adjective": "nice", "category" : "", "date": 123, "id": 8, "result": 200, "source_ip": "192.168.0.1", "time": 45, "type": "one", "user": "admin", "version": "2.0.1" }` ) func TestScanUniqueId(t *testing.T) { tests := []struct { inputs []string caseDesc string uniqueMessageProps []string expctdInvc int }{ { inputs: []string{unique_scan1, unique_scan1}, caseDesc: "Same scan twice with unique message props specified", uniqueMessageProps: []string{"digest", "image", "registry"}, expctdInvc: 1, }, { inputs: []string{unique_scan1, unique_scan1}, caseDesc: "Same scan twice without unique message props specified", expctdInvc: 2, }, { inputs: []string{unique_scan1, unique_scan2}, caseDesc: "2 unique scan with unique message props specified", uniqueMessageProps: []string{"digest", "image", "registry"}, expctdInvc: 2, }, { inputs: []string{unique_scan1, unique_scan2}, caseDesc: "2 unique scan without unique message props specified", uniqueMessageProps: []string{"digest", "image", "registry"}, expctdInvc: 2, }, { inputs: []string{non_unique_payload, non_unique_payload}, caseDesc: "2 non-scan inputs without unique message props specified", expctdInvc: 2, }, } for _, test := range tests { sendInputs(t, test.caseDesc, test.inputs, test.uniqueMessageProps, test.expctdInvc) } } func sendInputs(t *testing.T, caseDesc string, inputs []string, uniqueMessageProps []string, expected int) { dbPathReal := dbservice.DbPath defer func() { os.Remove(dbservice.DbPath) dbservice.ChangeDbPath(dbPathReal) }() dbservice.ChangeDbPath("test_webhooks.db") demoEmailAction := &DemoEmailAction{ emailCounts: 0, } srvUrl := "" demoRoute := &routes.InputRoute{} demoRoute.Name = "demo-route" demoRoute.Plugins.UniqueMessageProps = uniqueMessageProps demoInptEval := &DemoInptEval{} demoEmailAction.wg = &sync.WaitGroup{} demoEmailAction.wg.Add(expected) for _, inp := range inputs { srv := new(MsgService) if srv.EvaluateRegoRule(demoRoute, []byte(inp)) { srv.MsgHandling([]byte(inp), demoEmailAction, demoRoute, demoInptEval, &srvUrl) } } demoEmailAction.wg.Wait() if demoEmailAction.getEmailsCount() != expected { t.Errorf("[%s] Wrong number of Send method calls: expected %d, got %d", caseDesc, expected, demoEmailAction.getEmailsCount()) } } func TestGetMessageUniqueId(t *testing.T) { tests := []struct { props []string name string context map[string]interface{} filename string wantKey string wantErr string }{ { props: []string{"name"}, name: "Single property", context: map[string]interface{}{"name": "alpine"}, wantKey: "alpine", }, { props: []string{"name", "registry"}, name: "Multi property", context: map[string]interface{}{"name": "alpine", "registry": "registry2"}, wantKey: "alpine-registry2", }, { props: []string{"name", "cnt"}, name: "Numeric", context: map[string]interface{}{"name": "alpine", "cnt": 0}, wantKey: "alpine-0", }, { props: []string{"name", "registry"}, name: "Missed property", context: map[string]interface{}{"name": "alpine"}, wantKey: "alpine", }, { props: []string{"name", "meta.category"}, name: "Multi Level Property", context: map[string]interface{}{"name": "alpine", "meta": map[string]interface{}{"category": "design"}}, wantKey: "alpine-design", }, { props: []string{"name", "items.id"}, name: "Multi Level Property With Collection", context: map[string]interface{}{"name": "alpine", "items": []interface{}{map[string]interface{}{"id": "KLM"}, map[string]interface{}{"id": "DEF"}}}, wantKey: "alpine-KLM", }, { props: []string{"name", "items.id"}, name: "Multi Level Property With Empty Collection", context: map[string]interface{}{"name": "alpine", "items": []map[string]interface{}{}}, wantKey: "alpine", }, { props: []string{"name.id"}, name: "Multi Level Property Referencing String", context: map[string]interface{}{"name": "alpine"}, }, { props: []string{"digest", "image", "registry", "vulnerability_summary.critical", "vulnerability_summary.high", "vulnerability_summary.medium", "vulnerability_summary.low"}, name: "Legacy scan logic from Postee 1.0", filename: "all-in-one-image.json", wantKey: "sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf-all-in-one:3.5.19223-Aqua-0-7-30-6", }, { props: []string{"arr.foo"}, name: "Multi Level Property With Collection Of Interfaces", filename: "collection-of-interfaces.json", wantKey: "bar", }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var msg map[string]interface{} if test.filename != "" { fname := filepath.Join("testdata", test.filename) b, err := os.ReadFile(fname) require.NoError(t, err) err = json.Unmarshal(b, &msg) require.NoError(t, err) } else { msg = test.context } key := GetMessageUniqueId(msg, test.props) assert.Equal(t, test.wantKey, key) }) } } ================================================ FILE: msgservice/logs.go ================================================ package msgservice import "log" func prnInputLogs(msg string, v ...interface{}) { maxLen := 20 for idx, e := range v { b, ok := e.([]byte) if ok { if l := len(b); l > maxLen { v[idx] = string(b[:maxLen]) } } } log.Printf(msg, v...) } ================================================ FILE: msgservice/msghandling.go ================================================ package msgservice import ( "encoding/json" "log" "strings" "time" "github.com/aquasecurity/postee/v2/actions" "github.com/aquasecurity/postee/v2/data" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/regoservice" "github.com/aquasecurity/postee/v2/routes" ) type MsgService struct { } func (scan *MsgService) MsgHandling(input []byte, output actions.Action, route *routes.InputRoute, inpteval data.Inpteval, AquaServer *string) { if output == nil { return } in := map[string]interface{}{} if err := json.Unmarshal(input, &in); err != nil { prnInputLogs("json.Unmarshal error for %q: %v", input, err) return } //TODO move logic below somewhere close to Jira action implementation owners := "" applicationScopeOwnersObj, ok := in["application_scope_owners"] if ok { applicationScopeOwners := make([]string, 0) for _, owner := range applicationScopeOwnersObj.([]interface{}) { applicationScopeOwners = append(applicationScopeOwners, owner.(string)) } if len(applicationScopeOwners) > 0 { owners = strings.Join(applicationScopeOwners, ";") } } if route.Plugins.UniqueMessageProps != nil && len(route.Plugins.UniqueMessageProps) > 0 { msgKey := GetMessageUniqueId(in, route.Plugins.UniqueMessageProps) expired := calculateExpired(route.Plugins.UniqueMessageTimeoutSeconds) wasStored, err := dbservice.MayBeStoreMessage(input, msgKey, expired) if err != nil { log.Printf("Error while storing input: %v", err) return } if !wasStored { log.Printf("The same message was received before: %s", msgKey) return } } posteeOpts := map[string]string{ "AquaServer": *AquaServer, } in["postee"] = posteeOpts content, err := inpteval.Eval(in, *AquaServer) if err != nil { log.Printf("Error while evaluating input: %v", err) return } if owners != "" { content["owners"] = owners } if route.Plugins.AggregateMessageNumber > 0 && inpteval.IsAggregationSupported() { aggregated := AggregateScanAndGetQueue(route.Name, content, route.Plugins.AggregateMessageNumber, false) if len(aggregated) > 0 { content, err = inpteval.BuildAggregatedContent(aggregated) if err != nil { log.Printf("Error while building aggregated content: %v", err) return } if route.SerializeActions { send(output, content) } else { go send(output, content) } } } else if route.Plugins.AggregateTimeoutSeconds > 0 && inpteval.IsAggregationSupported() { AggregateScanAndGetQueue(route.Name, content, 0, true) if !route.IsSchedulerRun() { //TODO route shouldn't have any associated logic log.Printf("about to schedule %s\n", route.Name) RunScheduler(route, send, AggregateScanAndGetQueue, inpteval, &route.Name, output) } else { log.Printf("%s is already scheduled\n", route.Name) } } else { if route.SerializeActions { send(output, content) } else { go send(output, content) } } } // EvaluateRegoRule returns true in case the given input ([]byte) matches the input of the given route func (scan *MsgService) EvaluateRegoRule(r *routes.InputRoute, input []byte) bool { in := map[string]interface{}{} if err := json.Unmarshal(input, &in); err != nil { prnInputLogs("json.Unmarshal error for %q: %v", input, err) return false } // input-files will override input, if both are provided if ok, err := regoservice.DoesMatchRegoCriteria(in, r.InputFiles, r.Input); err != nil { if !regoservice.IsUsedRegoFiles(r.InputFiles) { prnInputLogs("Error while evaluating rego rule %s :%v for the input %s", r.Input, err, input) } else { prnInputLogs("Error while evaluating rego rule for input files :%v for the input %s", err, input) } return false } else if !ok { if !regoservice.IsUsedRegoFiles(r.InputFiles) { prnInputLogs("Input %s... doesn't match a REGO rule: %s", input, r.Input) } else { prnInputLogs("Input %s... doesn't match a REGO input files rule", input) } return false } return true } func send(otpt actions.Action, cnt map[string]string) { err := otpt.Send(cnt) if err != nil { log.Printf("Error while sending event: %v", err) } err = dbservice.RegisterPlgnInvctn(otpt.GetName()) if err != nil { log.Printf("Error while building aggregated content: %v", err) return } } func calculateExpired(UniqueMessageTimeoutSeconds int) *time.Time { if UniqueMessageTimeoutSeconds == 0 { return nil } timeToExpire := time.Duration(UniqueMessageTimeoutSeconds) * time.Second expired := time.Now().UTC().Add(timeToExpire) return &expired } var AggregateScanAndGetQueue = func(outputName string, currentContent map[string]string, counts int, ignoreLength bool) []map[string]string { aggregatedScans, err := dbservice.AggregateScans(outputName, currentContent, counts, ignoreLength) if err != nil { log.Printf("AggregateScans Error: %v", err) return aggregatedScans } if len(currentContent) != 0 && len(aggregatedScans) == 0 { log.Printf("New scan was added to the queue of %q without sending.", outputName) return nil } return aggregatedScans } ================================================ FILE: msgservice/msgservice_mocks_test.go ================================================ package msgservice import ( "log" "strings" "sync" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) var ( mockScan1 = `{"image":"Demo mock image1","registry":"registry1","vulnerability_summary":{"critical":0,"high":1,"medium":3,"low":4,"negligible":5},"image_assurance_results":{"disallowed":true}}` mockScan2 = `{"image":"Demo mock Image2","registry":"registry2","vulnerability_summary":{"critical":0,"high":0,"medium":3,"low":4,"negligible":5},"image_assurance_results":{"disallowed":false}}` mockScan3 = `{"image":"Demo mock Image3","registry":"Registry3","vulnerability_summary":{"critical":0,"high":0,"medium":0,"low":4,"negligible":5},"image_assurance_results":{"disallowed":true}}` mockScan4 = `{"image":"Demo mock image4","registry":"registry4","vulnerability_summary":{"critical":0,"high":0,"medium":0,"low":0,"negligible":5},"image_assurance_results":{"disallowed":true}}` ) type DemoInptEval struct { rndMu sync.Mutex aggrMu sync.Mutex renderCnt int aggrCnt int skipAggrSpprt bool } func (inptEval *DemoInptEval) Eval(in map[string]interface{}, serverUrl string) (map[string]string, error) { inptEval.rndMu.Lock() inptEval.renderCnt++ inptEval.rndMu.Unlock() title := "non-image" if img, ok := in["image"]; ok { title = img.(string) } return map[string]string{ "title": title, "description": title, }, nil } func (inptEval *DemoInptEval) BuildAggregatedContent(items []map[string]string) (map[string]string, error) { inptEval.aggrMu.Lock() inptEval.aggrCnt++ inptEval.aggrMu.Unlock() agrTitle := []string{} agrDescription := []string{} for _, item := range items { agrTitle = append(agrTitle, item["title"]) agrDescription = append(agrDescription, item["description"]) } return map[string]string{ "title": strings.Join(agrTitle, ","), "description": strings.Join(agrDescription, ","), }, nil } func (inptEval *DemoInptEval) IsAggregationSupported() bool { return !inptEval.skipAggrSpprt } type DemoEmailAction struct { wg *sync.WaitGroup mu sync.Mutex payloads []map[string]string emailCounts int } func (plg *DemoEmailAction) GetName() string { return "demo" } func (plg *DemoEmailAction) getEmailsCount() int { plg.mu.Lock() e := plg.emailCounts plg.mu.Unlock() return e } func (plg *DemoEmailAction) Init() error { return nil } func (plg *DemoEmailAction) Send(data map[string]string) error { log.Printf("Sending through demo plugin..\n") log.Printf("%s\n", data["title"]) plg.mu.Lock() plg.emailCounts++ plg.payloads = append(plg.payloads, data) plg.mu.Unlock() if plg.wg != nil { plg.wg.Done() } return nil } func (plg *DemoEmailAction) Terminate() error { return nil } func (plg *DemoEmailAction) GetLayoutProvider() layout.LayoutProvider { return new(formatting.HtmlProvider) } ================================================ FILE: msgservice/msgservice_scan_test.go ================================================ package msgservice import ( "strconv" "strings" "testing" "github.com/aquasecurity/postee/v2/data" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/layout" ) var ( AlpineImageResult = data.ScanImageInfo{ Image: "alpine:3.8", Registry: "Docker Hub", Digest: "sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1", PreviousDigest: "sha256:c8bccc0af9571ec0d006a43acb5a8d08c4ce42b6cc7194dd6eb167976f501ef1", ImageAssuranceResults: data.ImageAssuranceResults{ Disallowed: true, ChecksPerformed: []data.ControlCheck{ {Control: "max_severity", PolicyName: "Default", Failed: false}, {Control: "trusted_base_images", PolicyName: "Default", Failed: true}, {Control: "max_score", PolicyName: "Default", Failed: false}, }, }, VulnerabilitySummary: data.VulnerabilitySummary{ Total: 2, Critical: 0, High: 0, Medium: 2, Low: 0, Negligible: 0, Sensitive: 0, Malware: 0, }, ScanOptions: data.ScanOptions{ScanSensitiveData: true, ScanMalware: true}, Resources: []data.InfoResources{ { Vulnerabilities: []data.Vulnerability{ {Name: "CVE-2018-20679", Version: "", FixVersion: "", Severity: "medium"}, {Name: "CVE-2019-5747", Version: "", FixVersion: "", Severity: "medium"}, }, ResourceDetails: data.ResourceDetails{Name: "busybox", Version: "1.28.4-r3"}, }, }, } AshexPokemongoResult = data.ScanImageInfo{ Image: "ashex/pokemongo-map:latest", Registry: "Docker Hub", Digest: "sha256:ecc79e40b241b1b3b2580c58619cbc4c73b833308d780ad035bf6bdfbb529435", PreviousDigest: "sha256:ecc79e40b241b1b3b2580c58619cbc4c73b833308d780ad035bf6bdfbb529435", ImageAssuranceResults: data.ImageAssuranceResults{ Disallowed: true, ChecksPerformed: []data.ControlCheck{ {Control: "trusted_base_images", PolicyName: "Default", Failed: true}, {Control: "max_score", PolicyName: "Default", Failed: true}, {Control: "max_severity", PolicyName: "Default", Failed: true}, }, }, VulnerabilitySummary: data.VulnerabilitySummary{ Total: 249, Critical: 2, High: 52, Medium: 184, Low: 11, Negligible: 34, Sensitive: 15, Malware: 0, }, ScanOptions: data.ScanOptions{ScanSensitiveData: true, ScanMalware: true}, Resources: []data.InfoResources{ { Vulnerabilities: []data.Vulnerability{ {Name: "WS-2018-0076", Version: "", FixVersion: "0.6.0", Severity: "negligible"}, {Name: "", Version: "", FixVersion: "", Severity: ""}, }, ResourceDetails: data.ResourceDetails{Name: "", Version: ""}, }, }, ApplicationScopeOwners: []string{"recipient1@aquasec.com", "recipient1@aquasec.com"}, } ) func getImportantData(scan *data.ScanImageInfo) map[string]string { important := make(map[string]string) important[scan.Image] = "scan.Image" important[scan.Registry] = "" important[strconv.Itoa(scan.Critical)] = "scan.Critical" important[strconv.Itoa(scan.High)] = "scan.High" important[strconv.Itoa(scan.Medium)] = "scan.Medium" important[strconv.Itoa(scan.Low)] = "scan.Low" important[strconv.Itoa(scan.Negligible)] = "scan.Negligible" for _, resource := range scan.Resources { important[resource.Name] = "resource.Name" important[resource.ResourceDetails.Name] = "resource.ResourceDetails.Name" for _, vuln := range resource.Vulnerabilities { important[vuln.Name] = "vuln.Name" important[vuln.Version] = "vuln.Version" important[vuln.FixVersion] = "vuln.FixVersion" } } for i, check := range scan.ChecksPerformed { index := strconv.Itoa(i + 1) important[check.PolicyName] = index + ".check.PolicyName" important[check.Control] = index + ".check.Control" pass := "PASS" if check.Failed { pass = "FAIL" } important[pass] = pass } return important } func Equal(A, B *data.ScanImageInfo) bool { if A.Image != B.Image || A.Registry != B.Registry || A.ScanOptions != B.ScanOptions || A.VulnerabilitySummary != B.VulnerabilitySummary || A.ImageAssuranceResults.Disallowed != B.ImageAssuranceResults.Disallowed || len(A.ImageAssuranceResults.ChecksPerformed) != len(B.ImageAssuranceResults.ChecksPerformed) { return false } for i, v := range A.ImageAssuranceResults.ChecksPerformed { if B.ImageAssuranceResults.ChecksPerformed[i] != v { return false } } for i, v := range A.Resources { if len(v.Vulnerabilities) != len(B.Resources[i].Vulnerabilities) { return false } for j, vuln := range v.Vulnerabilities { if B.Resources[i].Vulnerabilities[j] != vuln { return false } } } return true } func BenchmarkGenTicketDescription(b *testing.B) { provider := new(formatting.JiraLayoutProvider) for i := 0; i < b.N; i++ { layout.GenTicketDescription(provider, &AlpineImageResult, nil, "https://demolab.aquasec.com/", "") } } func TestGenTicketDescription(t *testing.T) { var tests = []struct { currentScan *data.ScanImageInfo previousScan *data.ScanImageInfo }{ {&AlpineImageResult, nil}, {&AshexPokemongoResult, nil}, } providers := []layout.LayoutProvider{ new(formatting.JiraLayoutProvider), new(formatting.HtmlProvider), } for _, provider := range providers { for _, test := range tests { got := layout.GenTicketDescription(provider, test.currentScan, test.previousScan, "https://demolab.aquasec.com", "") important := getImportantData(test.currentScan) for k, v := range important { if !strings.Contains(got, k) { t.Errorf("Rendered data (%s) doesn't contain important value:\n%s (%s)\n", got, k, v) } } } } } func TestGenTicketDescriptionFieldSeeMore(t *testing.T) { var tests = []struct { name string serverUrl string image_url_part string expectedSuffix string }{ {"serverUrl is fill", "https://demolab.aquasec.com/", "alpine:3.9.6", "|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"}, {"serverUrl is empty", "", "alpine:3.9.6", "|CVE-2019-5747|busybox|1.28.4-r3|none|\n\n"}, } provider := new(formatting.JiraLayoutProvider) scan := &AlpineImageResult for _, test := range tests { got := layout.GenTicketDescription(provider, scan, nil, test.serverUrl, test.image_url_part) if !strings.HasSuffix(got, test.expectedSuffix) { t.Errorf("Rendered data doesn't have expected suffix:%s, got:%s", test.expectedSuffix, got) } } } ================================================ FILE: msgservice/msgservice_test.go ================================================ package msgservice import ( "errors" "os" "sync" "testing" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/routes" ) var ( invalidJson = `{ image : "My Image" }` ) type FailingInptEval struct { expectedError error expectedAggrError error } func (inptEval *FailingInptEval) Eval(in map[string]interface{}, serverUrl string) (map[string]string, error) { if inptEval.expectedError != nil { return nil, inptEval.expectedError } else { return map[string]string{ "title": "some title", "description": "some description", }, nil } } func (inptEval *FailingInptEval) BuildAggregatedContent(items []map[string]string) (map[string]string, error) { return nil, inptEval.expectedAggrError } func (inptEval *FailingInptEval) IsAggregationSupported() bool { return inptEval.expectedAggrError != nil } func TestInputs(t *testing.T) { tests := []struct { input []byte caseDesc string shouldPass bool }{ { input: nil, caseDesc: "Empty input", shouldPass: false, }, { input: []byte(invalidJson), caseDesc: "Invalid Json", shouldPass: false, }, } for _, test := range tests { validateInputValue(t, test.caseDesc, test.input, test.shouldPass) } } func validateInputValue(t *testing.T, caseDesc string, input []byte, shouldPass bool) { dbPathReal := dbservice.DbPath defer func() { os.Remove(dbservice.DbPath) dbservice.ChangeDbPath(dbPathReal) }() dbservice.ChangeDbPath("test_webhooks.db") demoEmailAction := &DemoEmailAction{ emailCounts: 0, } srvUrl := "" expected := 0 if shouldPass { expected = 1 } demoRoute := &routes.InputRoute{} demoRoute.Name = "demo-route" demoInptEval := &DemoInptEval{} demoEmailAction.wg = &sync.WaitGroup{} demoEmailAction.wg.Add(expected) srv := new(MsgService) if srv.EvaluateRegoRule(demoRoute, input) { srv.MsgHandling(input, demoEmailAction, demoRoute, demoInptEval, &srvUrl) } demoEmailAction.wg.Wait() if demoEmailAction.getEmailsCount() != expected { t.Errorf("[%s] Wrong number of Send method calls: expected %d, got %d", caseDesc, expected, demoEmailAction.getEmailsCount()) } } func TestEvalError(t *testing.T) { dbPathReal := dbservice.DbPath defer func() { os.Remove(dbservice.DbPath) dbservice.ChangeDbPath(dbPathReal) }() dbservice.ChangeDbPath("test_webhooks.db") demoEmailAction := &DemoEmailAction{ emailCounts: 0, } srvUrl := "" demoRoute := &routes.InputRoute{} demoRoute.Name = "demo-route" evalError := errors.New("eval error") demoInptEval := &FailingInptEval{ expectedError: evalError, } srv := new(MsgService) if srv.EvaluateRegoRule(demoRoute, []byte(mockScan1)) { srv.MsgHandling([]byte(mockScan1), demoEmailAction, demoRoute, demoInptEval, &srvUrl) } if demoEmailAction.getEmailsCount() > 0 { t.Errorf("Action shouldn't be called when evaluation is failed") } } func TestAggrEvalError(t *testing.T) { dbPathReal := dbservice.DbPath defer func() { os.Remove(dbservice.DbPath) dbservice.ChangeDbPath(dbPathReal) }() dbservice.ChangeDbPath("test_webhooks.db") demoEmailAction := &DemoEmailAction{ emailCounts: 0, } srvUrl := "" demoRoute := &routes.InputRoute{} demoRoute.Name = "demo-route" demoRoute.Plugins.AggregateMessageNumber = 2 aggrEvalError := errors.New("aggregation eval error") demoInptEval := &FailingInptEval{ expectedAggrError: aggrEvalError, } for i := 0; i < 2; i++ { srv := new(MsgService) if srv.EvaluateRegoRule(demoRoute, []byte(mockScan1)) { srv.MsgHandling([]byte(mockScan1), demoEmailAction, demoRoute, demoInptEval, &srvUrl) } } if demoEmailAction.getEmailsCount() > 0 { t.Errorf("Action shouldn't be called when evaluation is failed") } } func TestEmptyInput(t *testing.T) { dbPathReal := dbservice.DbPath defer func() { os.Remove(dbservice.DbPath) dbservice.ChangeDbPath(dbPathReal) }() dbservice.ChangeDbPath("test_webhooks.db") srvUrl := "" demoRoute := &routes.InputRoute{} demoRoute.Name = "demo-route" demoInptEval := &DemoInptEval{} srv := new(MsgService) if srv.EvaluateRegoRule(demoRoute, []byte("{}")) { srv.MsgHandling([]byte("{}"), nil, demoRoute, demoInptEval, &srvUrl) } if demoInptEval.renderCnt != 0 { t.Errorf("Eval() shouldn't be called if no output is passed to ResultHandling()") } } func TestMalformedJSON(t *testing.T) { var ( srvUrl = "" demoRoute = &routes.InputRoute{Name: "demo-route"} demoInptEval = &DemoInptEval{} demoEmailAction = &DemoEmailAction{} ) srv := new(MsgService) srv.MsgHandling([]byte("{test:test}"), demoEmailAction, demoRoute, demoInptEval, &srvUrl) if demoEmailAction.getEmailsCount() > 0 { t.Errorf("Action shouldn't be called when evaluation is failed") } } ================================================ FILE: msgservice/regocriteria_test.go ================================================ package msgservice import ( "os" "sync" "testing" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/routes" ) var ( badRego = ` default input = false hello { m := input.message m == "world" } ` correctRego = ` package postee default allow = false allow { contains(input.image, "image1") } ` ) func TestRegoCriteria(t *testing.T) { tests := []struct { input string caseDesc string regoCriteria string regoFilePath string shouldPass bool }{ { input: mockScan1, caseDesc: "Empty rule and files should allow", regoCriteria: "", regoFilePath: "", shouldPass: true, }, { input: mockScan1, caseDesc: "Matching rule", regoCriteria: `contains(input.image, "image1")`, regoFilePath: "", shouldPass: true, }, { input: mockScan2, caseDesc: "Not matching rule", regoCriteria: `contains(input.image, "image1")`, regoFilePath: "", shouldPass: false, }, { input: mockScan1, caseDesc: "Invalid rule", regoCriteria: badRego, regoFilePath: "", shouldPass: false, }, { input: mockScan1, caseDesc: "Matching file rule", regoCriteria: correctRego, regoFilePath: "../regoFile.rego", shouldPass: true, }, { input: mockScan2, caseDesc: "Not matching file rule", regoCriteria: correctRego, regoFilePath: "../regoFile.rego", shouldPass: false, }, { input: mockScan1, caseDesc: "Invalid file rule", regoCriteria: badRego, regoFilePath: "../regoFile.rego", shouldPass: false, }, } for _, test := range tests { validateRegoInput(t, test.caseDesc, test.input, test.regoCriteria, test.regoFilePath, test.shouldPass) } } func validateRegoInput(t *testing.T, caseDesc string, input string, regoCriteria string, regoFilePath string, shouldPass bool) { regoFile, err := os.Create("regoFile.rego") if err != nil { t.Error("Can't create regoFile.rego file") } _, err = regoFile.WriteString(regoCriteria) if err != nil { t.Error("Can't create regoFile.rego file") } defer os.Remove("regoFile.rego") defer regoFile.Close() dbPathReal := dbservice.DbPath defer func() { os.Remove(dbservice.DbPath) dbservice.ChangeDbPath(dbPathReal) }() dbservice.ChangeDbPath("test_webhooks.db") demoEmailAction := &DemoEmailAction{ emailCounts: 0, } srvUrl := "" expected := 0 if shouldPass { expected = 1 } demoRoute := &routes.InputRoute{} demoRoute.Name = "demo-route" demoRoute.Input = regoCriteria demoRoute.InputFiles = []string{regoFilePath} demoInptEval := &DemoInptEval{} demoEmailAction.wg = &sync.WaitGroup{} demoEmailAction.wg.Add(expected) srv := new(MsgService) if srv.EvaluateRegoRule(demoRoute, []byte(input)) { srv.MsgHandling([]byte(input), demoEmailAction, demoRoute, demoInptEval, &srvUrl) } demoEmailAction.wg.Wait() if demoEmailAction.getEmailsCount() != expected { t.Errorf("[%s] Wrong number of Send method calls: expected %d, got %d", caseDesc, expected, demoEmailAction.getEmailsCount()) } } ================================================ FILE: msgservice/scheduler.go ================================================ package msgservice import ( "log" "time" "github.com/aquasecurity/postee/v2/actions" "github.com/aquasecurity/postee/v2/data" "github.com/aquasecurity/postee/v2/routes" ) var getTicker = func(seconds int) *time.Ticker { return time.NewTicker(time.Duration(seconds) * time.Second) } var RunScheduler = func( route *routes.InputRoute, fnSend func(plg actions.Action, cnt map[string]string), fnAggregate func(outputName string, currentContent map[string]string, counts int, ignoreLength bool) []map[string]string, inpteval data.Inpteval, name *string, output actions.Action, ) { log.Printf("Scheduler is activated for route %q. Period: %d sec", route.Name, route.Plugins.AggregateTimeoutSeconds) ticker := getTicker(route.Plugins.AggregateTimeoutSeconds) route.StartScheduler() go func(done chan struct{}, currentTicker *time.Ticker) { for { select { case <-done: currentTicker.Stop() log.Printf("Scheduler for %q was stopped", route.Name) return case <-currentTicker.C: log.Printf("Scheduler triggered for %q", route.Name) queue := fnAggregate(route.Name, nil, 0, false) if len(queue) > 0 { aggregated, err := inpteval.BuildAggregatedContent(queue) if err != nil { log.Printf("Unable to build aggregated contents %v\n", err) } fnSend(output, aggregated) } } } }(route.Scheduling, ticker) //it has to be public to be used here. } ================================================ FILE: msgservice/scheduler_test.go ================================================ package msgservice import ( "sync" "testing" "time" "github.com/aquasecurity/postee/v2/actions" "github.com/aquasecurity/postee/v2/routes" "github.com/stretchr/testify/assert" ) func TestScheduler(t *testing.T) { routeName := "test-schedule" demoRoute := &routes.InputRoute{} demoRoute.Name = routeName demoRoute.Plugins.AggregateTimeoutSeconds = 3 demoSend := func(plg actions.Action, cnt map[string]string) { err := plg.Send(cnt) if err != nil { t.Fatal("error Send") } } tickerInvocations := 0 demoAggregate := func(outputName string, currentContent map[string]string, counts int, ignoreLength bool) []map[string]string { tickerInvocations++ return []map[string]string{ { "title": "title1", "description": "description1", }, { "title": "title2", "description": "description2", }, } } demoInptEval := &DemoInptEval{} demoEmailAction := &DemoEmailAction{ emailCounts: 0, } demoEmailAction.wg = &sync.WaitGroup{} demoEmailAction.wg.Add(1) RunScheduler(demoRoute, demoSend, demoAggregate, demoInptEval, &routeName, demoEmailAction) demoEmailAction.wg.Wait() demoRoute.StopScheduler() time.Sleep(time.Duration(2*demoRoute.Plugins.AggregateTimeoutSeconds) * time.Second) //make sure ticker is not invoked anymore assert.Equal(t, 1, tickerInvocations) } ================================================ FILE: msgservice/testdata/all-in-one-image.json ================================================ { "image": "all-in-one:3.5.19223", "registry": "Aqua", "scan_started": { "seconds": 1624544066, "nanos": 881635578 }, "scan_duration": 3, "pull_skipped": true, "image_size": 178041649, "digest": "sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf", "os": "alpine", "version": "3.8.4", "resources": [ { "resource": { "type": 2, "path": "/usr/local/bin/postgres", "name": "postgresql", "version": "9.5.14", "cpe": "cpe:/a:postgresql:postgresql:9.5.14", "layer_digest": "sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2018-1058", "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.", "nvd_score": 6.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1058", "publish_date": "2018-03-02", "modification_date": "2019-10-09", "nvd_score_v3": 8.8, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 6.5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 6.5", "aqua_score_classification": "NVD CVSS V2 Score: 6.5" }, { "name": "CVE-2018-1115", "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.", "nvd_score": 6.4, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:N/I:P/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1115", "publish_date": "2018-05-10", "modification_date": "2020-12-04", "fix_version": "9.6.9", "nvd_score_v3": 9.1, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H", "nvd_severity_v3": "critical", "aqua_score": 6.4, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:N/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 6.4", "aqua_score_classification": "NVD CVSS V2 Score: 6.4" }, { "name": "CVE-2018-16850", "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.", "nvd_score": 7.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "nvd_severity": "high", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-16850", "publish_date": "2018-11-13", "modification_date": "2019-10-09", "fix_version": "9.5.15", "nvd_score_v3": 9.8, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "critical", "aqua_score": 7.5, "aqua_severity": "high", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 7.5", "aqua_score_classification": "NVD CVSS V2 Score: 7.5" }, { "name": "CVE-2019-10130", "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.", "nvd_score": 4, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:S/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-10130", "publish_date": "2019-07-30", "modification_date": "2020-09-30", "fix_version": "9.5.17", "nvd_score_v3": 4.3, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", "nvd_severity_v3": "medium", "aqua_score": 4, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:S/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.0", "aqua_score_classification": "NVD CVSS V2 Score: 4.0" }, { "name": "CVE-2019-10208", "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.", "nvd_score": 6.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-10208", "publish_date": "2019-10-29", "modification_date": "2020-08-17", "fix_version": "9.5.19", "nvd_score_v3": 8.8, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 6.5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 6.5", "aqua_score_classification": "NVD CVSS V2 Score: 6.5" }, { "name": "CVE-2020-10733", "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.", "nvd_score": 4.4, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:L/AC:M/Au:N/C:P/I:P/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-10733", "publish_date": "2020-09-16", "modification_date": "2020-10-01", "fix_version": "9.5.22", "nvd_score_v3": 7.3, "nvd_vectors_v3": "CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 4.4, "aqua_severity": "medium", "aqua_vectors": "AV:L/AC:M/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.4", "aqua_score_classification": "NVD CVSS V2 Score: 4.4" }, { "name": "CVE-2020-14350", "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.", "nvd_score": 4.4, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:L/AC:M/Au:N/C:P/I:P/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-14350", "publish_date": "2020-08-24", "modification_date": "2020-09-18", "fix_version": "9.5.23", "nvd_score_v3": 7.3, "nvd_vectors_v3": "CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 4.4, "aqua_severity": "medium", "aqua_vectors": "AV:L/AC:M/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.4", "aqua_score_classification": "NVD CVSS V2 Score: 4.4" }, { "name": "CVE-2020-25694", "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.", "nvd_score": 6.8, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:P/I:P/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-25694", "publish_date": "2020-11-16", "modification_date": "2020-12-07", "fix_version": "9.5.24", "nvd_score_v3": 8.1, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 6.8, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 6.8", "aqua_score_classification": "NVD CVSS V2 Score: 6.8" }, { "name": "CVE-2020-25695", "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.", "nvd_score": 6.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-25695", "publish_date": "2020-11-16", "modification_date": "2020-12-07", "fix_version": "9.5.24", "nvd_score_v3": 8.8, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 6.5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:S/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 6.5", "aqua_score_classification": "NVD CVSS V2 Score: 6.5" }, { "name": "CVE-2020-25696", "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.", "nvd_score": 7.6, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:H/Au:N/C:C/I:C/A:C", "nvd_severity": "high", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-25696", "publish_date": "2020-11-23", "modification_date": "2020-12-15", "fix_version": "9.5.24", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 7.6, "aqua_severity": "high", "aqua_vectors": "AV:N/AC:H/Au:N/C:C/I:C/A:C", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 7.6", "aqua_score_classification": "NVD CVSS V2 Score: 7.6" }, { "name": "CVE-2021-3393", "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.", "nvd_score": 3.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:S/C:P/I:N/A:N", "nvd_severity": "low", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3393", "publish_date": "2021-04-01", "modification_date": "2021-06-04", "fix_version": "11.11", "nvd_score_v3": 4.3, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N", "nvd_severity_v3": "medium", "aqua_score": 3.5, "aqua_severity": "low", "aqua_vectors": "AV:N/AC:M/Au:S/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 3.5", "aqua_score_classification": "NVD CVSS V2 Score: 3.5" } ] }, { "resource": { "format": "apk", "name": "libgcrypt", "version": "1.8.3-r0", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:libgcrypt:1.8.3-r0", "license": "LGPL", "layer_digest": "sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785", "src_name": "libgcrypt", "src_version": "1.8.3-r0" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2019-12904", "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.", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-12904", "vendor_score_version": "CVSS v2", "publish_date": "2019-06-20", "modification_date": "2021-03-04", "fix_version": "1.8.3-r1", "solution": "Upgrade package libgcrypt to version 1.8.3-r1 or above.", "nvd_score_v3": 5.9, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "medium", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 328557, "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" }, { "name": "CVE-2021-33560", "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.", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-33560", "publish_date": "2021-06-08", "modification_date": "2021-06-15", "fix_version": "1.8.8", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "high", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" } ] }, { "resource": { "format": "apk", "name": "musl-utils", "version": "1.1.19-r10", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:musl-utils:1.1.19-r10", "license": "BSD,GPL2,MIT", "layer_digest": "sha256:c87736221ed0bcaa60b8e92a19bec2284899ef89226f2a07968677cf59e637a4", "src_name": "musl", "src_version": "1.1.19-r10" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2019-14697", "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.", "nvd_score": 7.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "nvd_severity": "high", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-14697", "vendor_score_version": "CVSS v2", "publish_date": "2019-08-06", "modification_date": "2020-03-14", "fix_version": "1.1.19-r11", "solution": "Upgrade package musl-utils to version 1.1.19-r11 or above.", "nvd_score_v3": 9.8, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "critical", "aqua_score": 7.5, "aqua_severity": "high", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 328567, "ancestor_pkg": "musl", "aqua_severity_classification": "NVD CVSS V2 Score: 7.5", "aqua_score_classification": "NVD CVSS V2 Score: 7.5" } ] }, { "resource": { "format": "apk", "name": "busybox", "version": "1.28.4-r3", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:busybox:1.28.4-r3", "license": "GPL2", "layer_digest": "sha256:c87736221ed0bcaa60b8e92a19bec2284899ef89226f2a07968677cf59e637a4", "src_name": "busybox", "src_version": "1.28.4-r3" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2018-1000517", "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.", "nvd_score": 7.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "nvd_severity": "high", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1000517", "publish_date": "2018-06-26", "modification_date": "2021-02-18", "fix_version": "1.29.0", "nvd_score_v3": 9.8, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "critical", "aqua_score": 7.5, "aqua_severity": "high", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 7.5", "aqua_score_classification": "NVD CVSS V2 Score: 7.5" }, { "name": "CVE-2018-20679", "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.", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-20679", "publish_date": "2019-01-09", "modification_date": "2019-09-04", "fix_version": "1.30.0", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "high", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" }, { "name": "CVE-2019-5747", "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.", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-5747", "publish_date": "2019-01-09", "modification_date": "2019-09-04", "already_acknowledged": true, "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "high", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" } ] }, { "resource": { "format": "apk", "name": "musl", "version": "1.1.19-r10", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:musl:1.1.19-r10", "license": "MIT", "layer_digest": "sha256:c87736221ed0bcaa60b8e92a19bec2284899ef89226f2a07968677cf59e637a4", "src_name": "musl", "src_version": "1.1.19-r10" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2019-14697", "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.", "nvd_score": 7.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "nvd_severity": "high", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-14697", "vendor_score_version": "CVSS v2", "publish_date": "2019-08-06", "modification_date": "2020-03-14", "fix_version": "1.1.19-r11", "solution": "Upgrade package musl to version 1.1.19-r11 or above.", "already_acknowledged": true, "nvd_score_v3": 9.8, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "critical", "aqua_score": 7.5, "aqua_severity": "high", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 328567, "aqua_severity_classification": "NVD CVSS V2 Score: 7.5", "aqua_score_classification": "NVD CVSS V2 Score: 7.5" }, { "name": "CVE-2020-28928", "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).", "nvd_score": 2.1, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P", "nvd_severity": "low", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-28928", "publish_date": "2020-11-24", "modification_date": "2021-06-08", "nvd_score_v3": 5.5, "nvd_vectors_v3": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H", "nvd_severity_v3": "medium", "aqua_score": 2.1, "aqua_severity": "low", "aqua_vectors": "AV:L/AC:L/Au:N/C:N/I:N/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 2.1", "aqua_score_classification": "NVD CVSS V2 Score: 2.1" } ] }, { "resource": { "format": "apk", "name": "bash", "version": "4.4.19-r1", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:bash:4.4.19-r1", "license": "GPL3orlater", "layer_digest": "sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785", "src_name": "bash", "src_version": "4.4.19-r1" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2019-18276", "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.", "nvd_score": 7.2, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:L/AC:L/Au:N/C:C/I:C/A:C", "nvd_severity": "high", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-18276", "publish_date": "2019-11-28", "modification_date": "2021-05-26", "nvd_score_v3": 7.8, "nvd_vectors_v3": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 7.2, "aqua_severity": "high", "aqua_vectors": "AV:L/AC:L/Au:N/C:C/I:C/A:C", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 7.2", "aqua_score_classification": "NVD CVSS V2 Score: 7.2" } ] }, { "resource": { "format": "apk", "name": "libcrypto1.0", "version": "1.0.2r-r0", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:libcrypto1.0:1.0.2r-r0", "license": "openssl", "layer_digest": "sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785", "src_name": "openssl", "src_version": "1.0.2r-r0" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2019-1547", "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).", "nvd_score": 1.9, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:L/AC:M/Au:N/C:P/I:N/A:N", "nvd_severity": "low", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1547", "vendor_score_version": "CVSS v2", "publish_date": "2019-09-10", "modification_date": "2020-10-20", "fix_version": "1.0.2t-r0", "solution": "Upgrade package libcrypto1.0 to version 1.0.2t-r0 or above.", "nvd_score_v3": 4.7, "nvd_vectors_v3": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "medium", "aqua_score": 1.9, "aqua_severity": "low", "aqua_vectors": "AV:L/AC:M/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 358269, "ancestor_pkg": "openssl", "aqua_severity_classification": "NVD CVSS V2 Score: 1.9", "aqua_score_classification": "NVD CVSS V2 Score: 1.9" }, { "name": "CVE-2019-1563", "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).", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1563", "vendor_score_version": "CVSS v2", "publish_date": "2019-09-10", "modification_date": "2020-10-20", "fix_version": "1.0.2t-r0", "solution": "Upgrade package libcrypto1.0 to version 1.0.2t-r0 or above.", "nvd_score_v3": 3.7, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", "nvd_severity_v3": "low", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 358270, "ancestor_pkg": "openssl", "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" }, { "name": "CVE-2019-1551", "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).", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1551", "vendor_score_version": "CVSS v2", "publish_date": "2019-12-06", "modification_date": "2021-06-14", "fix_version": "1.0.2u-r0", "solution": "Upgrade package libcrypto1.0 to version 1.0.2u-r0 or above.", "nvd_score_v3": 5.3, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", "nvd_severity_v3": "medium", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 581142, "ancestor_pkg": "openssl", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" }, { "name": "CVE-2019-1552", "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).", "nvd_score": 1.9, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:L/AC:M/Au:N/C:N/I:P/A:N", "nvd_severity": "low", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1552", "publish_date": "2019-07-30", "modification_date": "2020-12-23", "nvd_score_v3": 3.3, "nvd_vectors_v3": "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N", "nvd_severity_v3": "low", "aqua_score": 1.9, "aqua_severity": "low", "aqua_vectors": "AV:L/AC:M/Au:N/C:N/I:P/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 1.9", "aqua_score_classification": "NVD CVSS V2 Score: 1.9" }, { "name": "CVE-2020-1968", "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).", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-1968", "publish_date": "2020-09-09", "modification_date": "2021-06-14", "nvd_score_v3": 3.7, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", "nvd_severity_v3": "low", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" }, { "name": "CVE-2020-1971", "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).", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-1971", "publish_date": "2020-12-08", "modification_date": "2021-06-14", "nvd_score_v3": 5.9, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H", "nvd_severity_v3": "medium", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" }, { "name": "CVE-2021-23840", "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).", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-23840", "publish_date": "2021-02-16", "modification_date": "2021-06-17", "fix_version": "1.0.2y", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "nvd_severity_v3": "high", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" }, { "name": "CVE-2021-23841", "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).", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-23841", "publish_date": "2021-02-16", "modification_date": "2021-06-17", "fix_version": "1.0.2y", "nvd_score_v3": 5.9, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H", "nvd_severity_v3": "medium", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" } ] }, { "resource": { "format": "apk", "name": "libssl1.0", "version": "1.0.2r-r0", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:libssl1.0:1.0.2r-r0", "license": "openssl", "layer_digest": "sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785", "src_name": "openssl", "src_version": "1.0.2r-r0" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2019-1547", "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).", "nvd_score": 1.9, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:L/AC:M/Au:N/C:P/I:N/A:N", "nvd_severity": "low", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1547", "vendor_score_version": "CVSS v2", "publish_date": "2019-09-10", "modification_date": "2020-10-20", "fix_version": "1.0.2t-r0", "solution": "Upgrade package libssl1.0 to version 1.0.2t-r0 or above.", "nvd_score_v3": 4.7, "nvd_vectors_v3": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "medium", "aqua_score": 1.9, "aqua_severity": "low", "aqua_vectors": "AV:L/AC:M/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 358269, "ancestor_pkg": "openssl", "aqua_severity_classification": "NVD CVSS V2 Score: 1.9", "aqua_score_classification": "NVD CVSS V2 Score: 1.9" }, { "name": "CVE-2019-1563", "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).", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1563", "vendor_score_version": "CVSS v2", "publish_date": "2019-09-10", "modification_date": "2020-10-20", "fix_version": "1.0.2t-r0", "solution": "Upgrade package libssl1.0 to version 1.0.2t-r0 or above.", "nvd_score_v3": 3.7, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", "nvd_severity_v3": "low", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 358270, "ancestor_pkg": "openssl", "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" }, { "name": "CVE-2019-1551", "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).", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1551", "vendor_score_version": "CVSS v2", "publish_date": "2019-12-06", "modification_date": "2021-06-14", "fix_version": "1.0.2u-r0", "solution": "Upgrade package libssl1.0 to version 1.0.2u-r0 or above.", "nvd_score_v3": 5.3, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", "nvd_severity_v3": "medium", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 581142, "ancestor_pkg": "openssl", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" }, { "name": "CVE-2019-1552", "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).", "nvd_score": 1.9, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:L/AC:M/Au:N/C:N/I:P/A:N", "nvd_severity": "low", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-1552", "publish_date": "2019-07-30", "modification_date": "2020-12-23", "nvd_score_v3": 3.3, "nvd_vectors_v3": "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N", "nvd_severity_v3": "low", "aqua_score": 1.9, "aqua_severity": "low", "aqua_vectors": "AV:L/AC:M/Au:N/C:N/I:P/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 1.9", "aqua_score_classification": "NVD CVSS V2 Score: 1.9" }, { "name": "CVE-2020-1968", "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).", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-1968", "publish_date": "2020-09-09", "modification_date": "2021-06-14", "nvd_score_v3": 3.7, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", "nvd_severity_v3": "low", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" }, { "name": "CVE-2020-1971", "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).", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2020-1971", "publish_date": "2020-12-08", "modification_date": "2021-06-14", "nvd_score_v3": 5.9, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H", "nvd_severity_v3": "medium", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" }, { "name": "CVE-2021-23840", "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).", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-23840", "publish_date": "2021-02-16", "modification_date": "2021-06-17", "fix_version": "1.0.2y", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "nvd_severity_v3": "high", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" }, { "name": "CVE-2021-23841", "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).", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-23841", "publish_date": "2021-02-16", "modification_date": "2021-06-17", "fix_version": "1.0.2y", "nvd_score_v3": 5.9, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H", "nvd_severity_v3": "medium", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" } ] }, { "resource": { "format": "apk", "name": "libxml2", "version": "2.9.8-r1", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:libxml2:2.9.8-r1", "license": "MIT", "layer_digest": "sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785", "src_name": "libxml2", "src_version": "2.9.8-r1" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2019-19956", "description": "xmlParseBalancedChunkMemoryRecover in parser.c in libxml2 before 2.9.10 has a memory leak related to newDoc->oldNs.", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-19956", "vendor_score_version": "CVSS v2", "publish_date": "2019-12-24", "modification_date": "2021-04-20", "fix_version": "2.9.8-r2", "solution": "Upgrade package libxml2 to version 2.9.8-r2 or above.", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "nvd_severity_v3": "high", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:N/I:N/A:P", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 579780, "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" }, { "name": "CVE-2021-3517", "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.", "nvd_score": 7.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "nvd_severity": "high", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3517", "publish_date": "2021-05-19", "modification_date": "2021-06-14", "fix_version": "2.9.11", "nvd_score_v3": 8.6, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:H", "nvd_severity_v3": "high", "aqua_score": 7.5, "aqua_severity": "high", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 7.5", "aqua_score_classification": "NVD CVSS V2 Score: 7.5" }, { "name": "CVE-2021-3518", "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.", "nvd_score": 6.8, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:P/I:P/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3518", "publish_date": "2021-05-18", "modification_date": "2021-06-14", "fix_version": "2.9.11", "nvd_score_v3": 8.8, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 6.8, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 6.8", "aqua_score_classification": "NVD CVSS V2 Score: 6.8" }, { "name": "CVE-2021-3537", "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.", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-3537", "publish_date": "2021-05-14", "modification_date": "2021-06-14", "fix_version": "2.9.11", "nvd_score_v3": 5.9, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H", "nvd_severity_v3": "medium", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:N/I:N/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" } ] }, { "resource": { "format": "apk", "name": "libxslt", "version": "1.1.33-r1", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:libxslt:1.1.33-r1", "license": "custom", "layer_digest": "sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785", "src_name": "libxslt", "src_version": "1.1.33-r1" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2019-13117", "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.", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-13117", "vendor_score_version": "CVSS v2", "publish_date": "2019-07-01", "modification_date": "2020-08-24", "fix_version": "1.1.33-r3", "solution": "Upgrade package libxslt to version 1.1.33-r3 or above.", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "high", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 579832, "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" }, { "name": "CVE-2019-13118", "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.", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-13118", "vendor_score_version": "CVSS v2", "publish_date": "2019-07-01", "modification_date": "2020-08-24", "fix_version": "1.1.33-r3", "solution": "Upgrade package libxslt to version 1.1.33-r3 or above.", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "high", "aqua_score": 5, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 579836, "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" }, { "name": "CVE-2019-18197", "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.", "nvd_score": 5.1, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:H/Au:N/C:P/I:P/A:P", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-18197", "vendor_score_version": "CVSS v2", "publish_date": "2019-10-18", "modification_date": "2020-08-24", "fix_version": "1.1.33-r2", "solution": "Upgrade package libxslt to version 1.1.33-r2 or above.", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H", "nvd_severity_v3": "high", "aqua_score": 5.1, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:H/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 358259, "aqua_severity_classification": "NVD CVSS V2 Score: 5.1", "aqua_score_classification": "NVD CVSS V2 Score: 5.1" } ] } ], "image_assurance_results": { "checks_performed": [ { "policy_id": 1, "policy_name": "Default", "control": "malware" }, { "policy_id": 1, "policy_name": "Default", "control": "license" }, { "policy_id": 1, "policy_name": "Default", "control": "max_severity", "maximum_severity_allowed": "critical", "maximum_severity_found": "high" } ] }, "vulnerability_summary": { "total": 43, "critical": 0, "high": 7, "medium": 30, "low": 6, "negligible": 0, "sensitive": 0, "malware": 0, "score_average": 5.020931 }, "scan_options": { "scan_executables": true, "scan_sensitive_data": true, "show_will_not_fix": true, "webhook_url": "https://975cb1e5b1fc.ngrok.io", "scan_malware": true, "strict_scan": true, "scan_files": true, "scan_timeout": 3600000000000, "manual_pull_fallback": true, "dockerless": true, "enable_fast_scanning": true, "memoryThrottling": true, "suggest_os_upgrade": true, "seim_enabled": true }, "previous_digest": "sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf", "vulnerability_diff": { "total": 0, "critical": 0, "high": 0, "medium": 0, "low": 0, "negligible": 0, "sensitive": 0, "malware": 0 }, "initiating_user": "upwork", "data_date": 1624490283, "pull_name": "registry.aquasec.com/all-in-one:3.5.19223", "changed_result": false, "function_metadata": {}, "scan_id": 386815, "required_image_platform": "amd64:::", "scanned_image_platform": "amd64::linux:", "security_feeds_used": { "executables": "92475757e80429" }, "image_id": 704, "internal_digest_id": { "id": 1095 } } ================================================ FILE: msgservice/testdata/collection-of-interfaces.json ================================================ { "arr": [ { "foo": "bar" }, { "foo": "bar2" } ] } ================================================ FILE: msgservice/uniquemsgkey.go ================================================ package msgservice import ( "fmt" "strings" ) const ( propSep = "." ) func GetMessageUniqueId(in map[string]interface{}, props []string) string { values := make([]string, 0) for _, prop := range props { parts := strings.Split(prop, propSep) v := getSingleValue(in, parts) if v != "" { values = append(values, v) } } return strings.Join(values, "-") } func getSingleValue(o interface{}, parts []string) string { in, ok := o.(map[string]interface{}) if !ok { return "" } if len(parts) == 1 { v, ok := in[parts[0]] if ok { return fmt.Sprintf("%v", v) } } else { part := parts[0] v, ok := in[part] if ok { switch x := v.(type) { case map[string]interface{}: return getSingleValue(x, parts[1:]) case []interface{}: if len(x) > 0 { return getSingleValue(x[0], parts[1:]) //re-iterate with first element } } } } return "" } ================================================ FILE: overrides/main.html ================================================ {% extends "base.html" %} {% block outdated %} You're not viewing the latest version. Click here to go to latest. {% endblock %} ================================================ FILE: rego-filters/Allow-Image-Name.rego ================================================ package postee ArrayPermitedImageNames := {"ubuntu", "busybox"}#Comma separated list of images that will trigger the integration. default PermitImageNames = false PermitImageNames = true{ contains(input.image, ArrayPermitedImageNames[_]) } allow{ PermitImageNames } ================================================ FILE: rego-filters/Allow-Registry.rego ================================================ package postee ArrayPermitedRegistry := {"Aqua"} #The list of registry name that triggers the integration. default PermitRegistry = false PermitRegistry = true{ contains(input.registry, ArrayPermitedRegistry[_]) } allow{ PermitRegistry } ================================================ FILE: rego-filters/Credential Access ================================================ package postee ArrayBlockedSignaturesCredentialAccessCredentialAccess := { "TRC-8", "TRC-10" } default BlockedSignaturesCredentialAccess = false BlockedSignaturesCredentialAccess = true{ contains(input.SigMetadata.ID, ArrayBlockedSignaturesCredentialAccessCredentialAccess[_]) } allow{ BlockedSignaturesCredentialAccess } ================================================ FILE: rego-filters/Defense Evasion ================================================ package postee ArrayBlockedSignaturesDefenseEvation := { "TRC-2", "TRC-3", "TRC-4", "TRC-9", "TRC-5" } default BlockedSignaturesDefenseEvation = false BlockedSignaturesDefenseEvation = true{ contains(input.SigMetadata.ID, ArrayBlockedSignaturesDefenseEvation[_]) } allow{ BlockedSignaturesDefenseEvation } ================================================ FILE: rego-filters/Ignore-Image-Name.rego ================================================ package postee ArrayIgnoredImageNames := {"alpine", "postgres"} #List of comma separated images that will be ignored by the integration default IgnoreImageNames = true IgnoreImageNames = false{ contains(input.image, ArrayIgnoredImageNames[_]) } allow{ IgnoreImageNames } ================================================ FILE: rego-filters/Ignore-Registry.rego ================================================ package postee ArrayIgnoreRegistry := {"Aqua"} #Comma separated list of registries that will be ignored by the integration default IgnoreRegistry = true IgnoreRegistry = false{ contains(input.registry, ArrayIgnoreRegistry[_]) } allow{ IgnoreRegistry } ================================================ FILE: rego-filters/Initial Access ================================================ package postee ArrayBlockedSignaturesInitialAccess := { "TRC-12" } default BlockedSignaturesInitialAccess = false BlockedSignaturesInitialAccess = true{ contains(input.SigMetadata.ID, ArrayBlockedSignaturesInitialAccess[_]) } allow{ BlockedSignaturesInitialAccess } ================================================ FILE: rego-filters/Persistence ================================================ package postee ArrayBlockedSignaturesPersistence := { "TRC-7", "TRC-15" } default BlockedSignaturesPersistence = false BlockedSignaturesPersistence = true{ contains(input.SigMetadata.ID, ArrayBlockedSignaturesPersistence[_]) } allow{ BlockedSignaturesPersistence } ================================================ FILE: rego-filters/Policy-Min-Vulnerability.rego ================================================ package postee import future.keywords.in #Constants vulnerability values. Don't remove it! allVulnerability := {"negligible": 0, "low": 1, "medium": 2, "high": 3, "critical": 4} Vulnerability := "critical"#The minimum vulnerability severity that triggers the integration. default PermitMinVulnerability = false PermitMinVulnerability = true{ some i, val in allVulnerability val >= allVulnerability[Vulnerability] input.vulnerability_summary[i] > 0 } allow{ PermitMinVulnerability } ================================================ FILE: rego-filters/Policy-Only-Fix-Available.rego ================================================ package postee #Trigger the integration only if image has a vulnerability with fix available (true). #If set to false, integration will be triggered even if all vulnerabilities has no fix available default PermitOnlyFixAvailable = false PermitOnlyFixAvailable = true{ is_string(input.resources[_].vulnerabilities[_].fix_version) } allow{ PermitOnlyFixAvailable } ================================================ FILE: rego-filters/Policy-Related-Features.rego ================================================ package postee import future.keywords.in #Constants vulnerability values. Don't remove it! allVulnerability := {"negligible": 0, "low": 1, "medium": 2, "high": 3, "critical": 4} ArrayPermitedImageNames := {"ubuntu", "busybox"} #Comma separated list of images that will trigger the integration. ArrayIgnoredImageNames := {"alpine", "postgres"} #List of comma separated images that will be ignored by the integration ArrayPermitedRegistry := {"Aqua"} #The list of registry name that triggers the integration. ArrayIgnoreRegistry := {"Aqua"} #Comma separated list of registries that will be ignored by the integration Vulnerability := "low" #The minimum vulnerability severity that triggers the integration. default PermitImageNames = false PermitImageNames = true{ contains(input.image, ArrayPermitedImageNames[_]) } default IgnoreImageNames = true IgnoreImageNames = false{ contains(input.image, ArrayIgnoredImageNames[_]) } default PermitRegistry = false PermitRegistry = true{ contains(input.registry, ArrayPermitedRegistry[_]) } default IgnoreRegistry = true IgnoreRegistry = false{ contains(input.registry, ArrayIgnoreRegistry[_]) } default PermitMinVulnerability = false PermitMinVulnerability = true{ some i, val in allVulnerability val >= allVulnerability[Vulnerability] input.vulnerability_summary[i] > 0 } default PermitOnlyFixAvailable = false PermitOnlyFixAvailable = true{ is_string(input.resources[_].vulnerabilities[_].fix_version) } #Select the required functions. The functions will be conjunct as a logical "AND". allow{ # PermitImageNames # IgnoreImageNames # PermitRegistry # IgnoreRegistry # PermitMinVulnerability # PermitOnlyFixAvailable } ================================================ FILE: rego-filters/Privilege Escalation ================================================ package postee ArrayBlockedSignaturesPrivilegeEscalation := { "TRC-11", "TRC-14" } default BlockedSignaturesPrivilegeEscalation = false BlockedSignaturesPrivilegeEscalation = true{ contains(input.SigMetadata.ID, ArrayBlockedSignaturesPrivilegeEscalation[_]) } allow{ BlockedSignaturesPrivilegeEscalation } ================================================ FILE: rego-filters/Tracee Default Set ================================================ package postee ArrayBlockedSignatures := { "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" } default ArrayBlockedSignatures = false ArrayBlockedSignatures = true{ contains(input.SigMetadata.ID, ArrayBlockedSignatures[_]) } allow{ ArrayBlockedSignatures } ================================================ FILE: rego-filters/Trivy AWS Findings ================================================ package postee allow { contains(input.Findings[0].ProductFields["Product Name"], "Trivy") } ================================================ FILE: rego-templates/common/common.rego ================================================ package postee ############################################# Common functions ############################################ by_flag(a, b, flag) = a { flag } by_flag(a, b, flag) = b { flag = false } duplicate(a, b, col) = a {col == 1} duplicate(a, b, col) = b {col == 2} clamp(a, b) = b { a > b } clamp(a, b) = a { a <= b } flat_array(a) = o { o:=[item | item:=a[_][_] ] } with_default(obj, prop, default_value) = default_value{ not obj[prop] } with_default(obj, prop, default_value) = obj[prop]{ obj[prop] } ================================================ FILE: rego-templates/example/audit-html.rego ================================================ package example.audit.html #Example of handling audit user title:="Audit event received" result:=sprintf("Audit event received from %s", [input.user]) ================================================ FILE: rego-templates/example/defectdojo/trivy-operator-defectdojo.rego ================================================ # METADATA # title: trivy-operator-defectdojo # scope: package package plejd.trivyoperator.defectdojo title:="-" #not used with webhook # allow environments or any other input being mapped # to DefectDojo specific meta data, input data can be consumed # from report labels applied by Trivy-operator map_env_2_engagement := { "development": { "id": 200, "name": "engagement-dev" }, "production": { "id": 201, "name": "engagement-prod" }, "sandbox": { "id": 133, "name": "engagement-plejdground" }, "stage": { "id": 144, "name": "engagement-stage" }, } # this following JSON structure's format is dictated by how the # underlying CURL command is expecting the incoming JSON payload # to look like. It uses mainly two components - `report` and # `metadata`. dd_data := { "defectdojo": { "scan": input, "metadata": { "active": true, "engagement": map_env_2_engagement[input.metadata.labels.env].id, "engagement_name": map_env_2_engagement[input.metadata.labels.env].name, "environment": input.metadata.labels.env, "minimum_severity": "Medium", "product": "cluster", "scan_date": input.metadata.creationTimestamp, "scan_type": "Trivy Operator Scan", "test_title": "", "verified": true } } } # METADATA # entrypoint: true # description: | # Mangle a trivyoperator report and prepare it for being sent to DefectDojo. # Note, that everything under the key defectdojo.metadata will be added as # own FORM into the HTTP request sent to DefectDojo. # related_resources: # - ref: https://defectdojo.dev.plejd.io/api/v2/oa3/swagger-ui/ # description: "Plejd DefectDojo instance, swagger API docs" # organizations: # - Plejd AB # authors: # - name: Plejd CloudOps # email: team-cloudops@plejd.com result:=dd_data ================================================ FILE: rego-templates/example/defectdojo/trivy-operator-defectdojo_test.rego ================================================ package plejd.trivyoperator.defectdojo.test import data.plejd.trivyoperator.defectdojo.result test_a_allowed { input_data := { "kind": "ClusterRbacAssessmentReport", "metadata": { "labels": { "env": "development" }, "creationTimestamp": "1234567890" } } exp_data := { "defectdojo": { "scan": input_data, "metadata": { "active": true, "engagement": 200, "engagement_name": "engagement-dev", "environment": input_data.metadata.labels.env, "minimum_severity": "Medium", "product": "cluster", "scan_date": input_data.metadata.creationTimestamp, "scan_type": "Trivy Operator Scan", "test_title": "", "verified": true } } } result == exp_data with input as input_data } todo_test_false { false } ================================================ FILE: rego-templates/raw-message-html.rego ================================================ package postee.rawmessage.html title:="Raw Message Received" # Postee injects custom function jsonformat() to pretty print objects result:=sprintf("
%s
",[jsonformat(input)]) ================================================ FILE: rego-templates/raw-message-json.rego ================================================ package postee.rawmessage.json title:="-" #not used with webhook result:=jsonformat(input) ================================================ FILE: rego-templates/servicenow-incident.rego ================================================ package postee.servicenow.incident import future.keywords import data.postee.by_flag import data.postee.with_default ################################################ Templates ################################################ result_tpl = `

Name: %s

Category: %s

Severity: %s

Data: %s

Resourse policy name: %s

Resourse policy application scopes: %s

` summary_tpl =`Category: %s Severity: %s` table_tpl:=` %s
` cell_tpl:=`%s ` header_tpl:=`%s ` row_tpl:=` %s ` ########################################################################################################### title = input.name aggregation_pkg := "postee.vuls.html.aggregation" data_list(d) := list { dat := split(d, ",\"") some i list := [r | without_slash := replace(dat[i], "\"", "") without_open_bkt := replace(without_slash, "{", "") without_close_bkt := replace(without_open_bkt, "}", "") s := split(without_close_bkt, ":") value_with_colon := trim_left(without_close_bkt, sprintf("%s", [s[0]])) s[0] != "tracee_finding" r := [s[0], trim_left(value_with_colon, ":")] ] } render_table_headers(headers) = row { count(headers) > 0 ths := [th | header := headers[_] th := sprintf(header_tpl, [header]) ] row := sprintf(row_tpl, [concat("", ths)]) } render_table_headers(headers) = "" { #if headers not specified return empty results count(headers) == 0 } render_table(headers, content_array) = s { rows := [tr | cells:=content_array[_] tds:= [td | ctext:=cells[_] td := to_cell(ctext) ] tr=sprintf(row_tpl, [concat("", tds)]) ] s:=sprintf(table_tpl, [concat("", array.concat([render_table_headers(headers)],rows))]) } ## why I added it? to_cell(txt) = c { c:= sprintf(cell_tpl, [txt]) } found_data := with_default(input,"data", "") found_severity := "unknown" if{ with_default(input,"severity_score", "") == "" }else = format_int(input.severity_score, 10) ############################################## result values ############################################# result := res{ res = sprintf(result_tpl,[ with_default(input,"name", "name not found"), with_default(input,"category", "category not found"), found_severity, by_flag( "data not found", render_table([], data_list(found_data)), found_data == ""), with_default(input,"response_policy_name", "response policy name not found"), with_default(input,"application_scope", "none"), ]) } result_date = input.time result_category = "Security incident" result_assigned_to := by_flag(input.application_scope_owners[0], "", count(input.application_scope_owners) == 1) result_assigned_group := by_flag(input.application_scope[0], "", count(input.application_scope) == 1) result_severity := input.severity_score result_summary := summary{ summary = sprintf(summary_tpl,[ with_default(input,"category", "category not found"), found_severity, ]) } ================================================ FILE: rego-templates/servicenow-insight.rego ================================================ package postee.servicenow.insight import future.keywords import future.keywords.if import data.postee.by_flag import data.postee.with_default ################################################ Templates ################################################ #main template to render message html_tpl:=`

Insight Details:

Insight ID: %s

Description: %s

Impact: %s

Severity: %s

Found Date: %s

Last Scan: %s

URL: %s

Resourse Details:

Resourse ID: %s

Resourse Name: %s

ARN: %s

Extra Info: %s

Evidence:

%s %s

Recommendation:

%s

Resourse policy name: %s

Resourse policy application scopes: %s

` summary_tpl =`Insight ID: %s Description: %s Impact: %s Severity: %s Found Date: %s Last Scan: %s URL: %s` vlnrb_tpl = `

Vulnerabilities

%s ` sensitive_data_tpl = `

Sensitive data

%s ` #Extra % is required in width:100% table_tpl:=` %s
` cell_tpl:=`%s ` header_tpl:=`%s ` row_tpl:=` %s ` ########################################################################################################### ############################################## Html rendering ############################################# render_table_headers(headers) = row { count(headers) > 0 ths := [th | header := headers[_] th := sprintf(header_tpl, [header]) ] row := sprintf(row_tpl, [concat("", ths)]) } render_table_headers(headers) = "" { #if headers not specified return empty results count(headers) == 0 } render_table(headers, content_array) = s { rows := [tr | cells:=content_array[_] tds:= [td | ctext:=cells[_] td := to_cell(ctext) ] tr=sprintf(row_tpl, [concat("", tds)]) ] s:=sprintf(table_tpl, [concat("", array.concat([render_table_headers(headers)],rows))]) } ## why I added it? to_cell(txt) = c { c:= sprintf(cell_tpl, [txt]) } ####################################### Template specific functions ####################################### # TODO refactor to support different properties check_failed(item) = false { not item.failed #Either absent or false } check_failed(item) = true { item.failed } ################################### Vulnerability table ############################################## vlnrb_headers := ["Vulnerability ID", "Severity", "Resource name", "Installed version", "Fix version"] render_vlnrb(list) = sprintf(vlnrb_tpl, [render_table(vlnrb_headers, list)]) { count(list) > 0 } render_vlnrb(list) = "" { #returns empty string if list of vulnerabilities is passed count(list) == 0 } vln_list = vlnrb { some i vlnrb := [r | vlnname := input.evidence.vulnerabilities[i].name severity := input.evidence.vulnerabilities[i].severity fxvrsn := with_default(input.evidence.vulnerabilities[i],"fix_version", "none") package_name = with_default(input.evidence.vulnerabilities[i], "package_name", "none") package_version = with_default(input.evidence.vulnerabilities[i], "current_version", "none") r := [vlnname, severity, package_name, package_version, fxvrsn] ] } ################################### Sensitive data table ############################################## sensitive_data_headers := ["File Type", "File Path", "Image"] render_sensitive_data(list) = sprintf(sensitive_data_tpl, [render_table(sensitive_data_headers, list)]) { count(list) > 0 } render_sensitive_data(list) = "" { #returns empty string if list of sensitive data is passed count(list) == 0 } sensitive_data_list = vlnrb { some i vlnrb := [r | file_type := input.evidence.sensitive_data[i].file_type file_path := input.evidence.sensitive_data[i].file_path image := input.evidence.sensitive_data[i].image r := [file_type, file_path, image] ] } ########################################################################################################### postee := with_default(input, "postee", {}) aqua_server := with_default(postee, "AquaServer", "") server_url := trim_suffix(aqua_server, "/#/images/") title = input.insight.description href := sprintf("%s/ah/#/%s/%s/%s/%s", [server_url, "insights", urlquery.encode(input.insight.id), "resource", urlquery.encode(input.resource.id)]) text := sprintf("%s/ah/#/%s/%s/%s/%s", [server_url, "insights", input.insight.id, "resource", input.resource.id]) aggregation_pkg := "postee.vuls.html.aggregation" priority_as_text = "critical" if { input.insight.priority == 4 }else = "high" if { input.insight.priority == 3 }else = "medium" if { input.insight.priority == 2 }else = "low" if { input.insight.priority == 1 }else = "negligible" if { input.insight.priority == 0 }else = "unknown" remediation_with_default(default_value) = default_value{ input.evidence.vulnerabilities_remediation==null; input.evidence.sensitive_data_remediation==""; input.evidence.malware_remediation=="" } remediation_with_default(default_value) = val{ val := input.evidence.vulnerabilities_remediation input.evidence.vulnerabilities_remediation!=null; input.evidence.sensitive_data_remediation==""; input.evidence.malware_remediation=="" } remediation_with_default(default_value) = val{ val := input.evidence.vulnerabilities_remediation input.evidence.vulnerabilities_remediation!=null; input.evidence.sensitive_data_remediation!=""; input.evidence.malware_remediation=="" } remediation_with_default(default_value) = val{ val := input.evidence.sensitive_data_remediation val !="";input.evidence.vulnerabilities_remediation==null; input.evidence.malware_remediation=="" } remediation_with_default(default_value) = val{ val := input.evidence.malware_remediation val != ""; input.evidence.vulnerabilities_remediation==null; input.evidence.sensitive_data_remediation=="" } ############################################## result values ############################################# result = msg { msg := sprintf(html_tpl, [ input.insight.id, input.insight.description, input.insight.impact, priority_as_text, input.resource.found_date, input.resource.last_scanned, by_flag( "", sprintf(`%s`,[href, text]), #link server_url == ""), input.resource.id, input.resource.name, input.resource.arn, input.resource.steps, render_vlnrb(vln_list), render_sensitive_data(sensitive_data_list), remediation_with_default("No Recommendation"), input.response_policy_name, with_default(input,"application_scope", "none"), ]) } result_category = "Security insight" result_assigned_to := by_flag(input.application_scope_owners[0], "", count(input.application_scope_owners) == 1) result_assigned_group := by_flag(input.application_scope[0], "", count(input.application_scope) == 1) result_severity := input.insight.priority result_summary := summary{ summary = sprintf(summary_tpl,[ input.insight.id, input.insight.description, input.insight.impact, priority_as_text, input.resource.found_date, input.resource.last_scanned, by_flag( "", text, #link server_url == ""), ]) } ================================================ FILE: rego-templates/servicenow.rego ================================================ package postee.servicenow import future.keywords import future.keywords.if import data.postee.by_flag import data.postee.with_default ################################################ Templates ################################################ #main template to render message html_tpl:=`

Name: %s

Registry: %s

Malware found: %s

Sensitive data found: %s

Vulnerability summary

%s %s %s %s %s %s %s

Resourse policy name: %s

Resourse policy application scopes: %s

%s ` summary_tpl =`Name: %s Registry: %s %s %s vulnerabilities: * critical: %d, * high: %d, * medium: %d, * low: %d, * negligible: %d %s` vlnrb_tpl = `

%s severity vulnerabilities

%s ` assurance_control_tpl = `

Assurance controls

%s ` #Extra % is required in width:100% table_tpl:=` %s
` cell_tpl:=`%s ` header_tpl:=`%s ` row_tpl:=` %s ` colored_text_tpl:="%s" ########################################################################################################### ############################################## Html rendering ############################################# render_table_headers(headers) = row { count(headers) > 0 ths := [th | header := headers[_] th := sprintf(header_tpl, [header]) ] row := sprintf(row_tpl, [concat("", ths)]) } render_table_headers(headers) = "" { #if headers not specified return empty results count(headers) == 0 } render_table(headers, content_array) = s { rows := [tr | cells:=content_array[_] tds:= [td | ctext:=cells[_] td := to_cell(ctext) ] tr=sprintf(row_tpl, [concat("", tds)]) ] s:=sprintf(table_tpl, [concat("", array.concat([render_table_headers(headers)],rows))]) } ## why I added it? to_cell(txt) = c { c:= sprintf(cell_tpl, [txt]) } to_colored_text(color, txt) = spn { spn :=sprintf(colored_text_tpl, [color, txt]) } ####################################### Template specific functions ####################################### to_severity_color(color, level) = spn { spn:=to_colored_text(color, format_int(with_default(input.vulnerability_summary,level,0), 10)) } # TODO refactor to support different properties check_failed(item) = false { not item.failed #Either absent or false } check_failed(item) = true { item.failed } # 2 dimension array for vulnerabilities summary severities_stats := [ ["critical", to_severity_color("#c00000", "critical")], ["high", to_severity_color("#e0443d", "high")], ["medium", to_severity_color("#f79421", "medium")], ["low", to_severity_color("#e1c930", "low")], ["negligible", to_severity_color("green", "negligible")] ] # 2 dimension array for assurance controls assurance_controls := [ control | item := input.image_assurance_results.checks_performed[i] control := [format_int(i+1, 10), item.control,item.policy_name, by_flag( "FAIL", "PASS", check_failed(item) ) ] ] vlnrb_headers := ["Vulnerability ID", "Resource name", "Installed version", "Fix version"] render_vlnrb(severity, list) = sprintf(vlnrb_tpl, [severity, render_table(vlnrb_headers, list)]) { count(list) > 0 } render_vlnrb(severity, list) = "" { #returns empty string if list of vulnerabilities is passed count(list) == 0 } assurance_control_headers := ["#","Control","Policy Name", "Status"] render_assurance_control(list) = sprintf(assurance_control_tpl, [render_table(assurance_control_headers, list)]) { count(list) > 0 } render_assurance_control(list) = "" { #returns empty string if list of assurance control is passed count(list) == 0 } # builds 2-dimension array for vulnerability table vln_list(severity) = vlnrb { some i, j vlnrb := [r | item := input.resources[i] resource := item.resource vlnname := item.vulnerabilities[j].name fxvrsn := with_default(item.vulnerabilities[j],"fix_version", "none") resource_name = with_default(resource, "name", "none") resource_version = with_default(resource, "version", "none") item.vulnerabilities[j].aqua_severity == severity # only items with severity matched r := [vlnname, resource_name, resource_version, fxvrsn] ] } ########################################################################################################### postee := with_default(input, "postee", {}) aqua_server := with_default(postee, "AquaServer", "") server_url := trim_suffix(aqua_server, "images/") report_type := "function" if{ input.entity_type == 1 } else = "vm" if{ input.entity_type == 2 } else = "image" title = sprintf(`Aqua security | %s | %s | Scan report`, [report_type, input.image]) ## url formats: ## function: /#/functions// ## vm: /#/infrastructure//node ## image: /#/image// href := sprintf("%s%s/%s/%s", [server_url, "functions", urlquery.encode(input.registry), urlquery.encode(input.image)]) if{ report_type == "function" } else = sprintf("%s%s/%s/%s", [server_url, "infrastructure", urlquery.encode(input.image), "node"]){ report_type == "vm" } else = sprintf("%s%s/%s/%s", [server_url, "image", urlquery.encode(input.registry), urlquery.encode(input.image)]) text := sprintf("%s%s/%s/%s", [server_url, "functions", input.registry, input.image]) if{ report_type == "function" } else = sprintf("%s%s/%s/%s", [server_url, "infrastructure", input.image, "node"]) { report_type == "vm" } else = sprintf("%s%s/%s/%s", [server_url, report_type, input.registry, input.image]) url := by_flag("", href, server_url == "") # some vulnerability_summary fields may not exist default vulnerability_summary_critical := 0 vulnerability_summary_critical := input.vulnerability_summary.critical default vulnerability_summary_high := 0 vulnerability_summary_high := input.vulnerability_summary.high default vulnerability_summary_medium := 0 vulnerability_summary_medium := input.vulnerability_summary.medium default vulnerability_summary_low := 0 vulnerability_summary_low := input.vulnerability_summary.low default vulnerability_summary_negligible := 0 vulnerability_summary_negligible := input.vulnerability_summary.negligible aggregation_pkg := "postee.vuls.html.aggregation" ############################################## result values ############################################# result = msg { msg := sprintf(html_tpl, [ input.image, input.registry, by_flag( "Yes", "No", input.scan_options.scan_malware #reflects current logic ), by_flag( "Yes", "No", input.scan_options.scan_sensitive_data #reflects current logic ), render_table([], severities_stats), render_assurance_control(assurance_controls), render_vlnrb("Critical", vln_list("critical")), render_vlnrb("High", vln_list("high")), render_vlnrb("Medium", vln_list("medium")), render_vlnrb("Low", vln_list("low")), render_vlnrb("Negligible", vln_list("negligible")), with_default(input,"response_policy_name", ""), with_default(input,"application_scope", "none"), by_flag( "", sprintf(`

See more: %s

`,[href, text]), #link server_url == "") ]) } result_date = input.scan_started.seconds result_category = "Serverless functions Scanning" if { report_type == "function" }else = "Security - VM Scan results" if { report_type == "vm" }else = "Security Image Scan results" result_subcategory = "Security incident" result_assigned_to := by_flag(input.application_scope_owners[0], "", count(input.application_scope_owners) == 1) result_assigned_group := by_flag(input.application_scope[0], "", count(input.application_scope) == 1) result_severity := 1 if { input.vulnerability_summary.critical > 0 } else = 2 if { input.vulnerability_summary.high > 0 } else = 3 result_summary := summary{ summary = sprintf(summary_tpl,[ input.image, input.registry, by_flag( "Malware found: Yes", "Malware found: No", input.scan_options.scan_malware #reflects current logic ), by_flag( "Sensitive data found: Yes", "Sensitive data found: No", input.scan_options.scan_sensitive_data #reflects current logic ), vulnerability_summary_critical, vulnerability_summary_high, vulnerability_summary_medium, vulnerability_summary_low, vulnerability_summary_negligible, by_flag( "", sprintf(`See more: %s`,[text]), #link server_url == ""), ]) } ================================================ FILE: rego-templates/tracee-html.rego ================================================ package postee.tracee.html #Example of handling tracee event title:=sprintf("Tracee Detection - %s", [input.SigMetadata.Name]) tpl :=`

Rule Description: %s

Detection: %s

MITRE Details: %s

Severity: %v

` result:= res { res:= sprintf(tpl, [ input.SigMetadata.Description, input.Context.processName, input.SigMetadata.Properties, input.SigMetadata.Properties.Severity ]) } ================================================ FILE: rego-templates/tracee-slack.rego ================================================ package postee.tracee.slack #Example of handling tracee event title:=sprintf("Tracee Detection - %s", [input.SigMetadata.Name]) result:= res { res:= [ { "type":"section", "text": {"type":"mrkdwn","text": sprintf("*Rule Description:* %s", [input.SigMetadata.Description])}}, { "type":"section", "text": {"type":"mrkdwn","text": sprintf("*Detection:* %s", [input.Context.processName])}}, { "type":"section", "text": {"type":"mrkdwn","text": sprintf("*MITRE Details:* %v", [input.SigMetadata.Properties])}}, { "type":"section", "text": {"type":"mrkdwn","text": sprintf("*Severity:* %v", [input.SigMetadata.Properties.Severity])}} ] } ================================================ FILE: rego-templates/trivy-jira.rego ================================================ package postee.trivy.jira ############################################# Common functions ############################################ with_default(obj, prop, default_value) = default_value { not obj[prop] } with_default(obj, prop, default_value) = obj[prop] { obj[prop] } #import common.by_flag ################################################ Templates ################################################ #main template to render message tpl:=` h1. Image name: %s %s %s %s %s %s ` vlnrb_tpl = ` h4. %s severity vulnerabilities %s ` #Extra % is required in width:100% table_tpl := ` %s ` cell_tpl := `| %s ` header_tpl := `|| %s ` row_tpl := ` | %s ` colored_text_tpl := "{color:%s}%s{color}" ########################################################################################################### ############################################## Html rendering ############################################# render_table_headers(headers) = row { count(headers) > 0 ths := [th | header := headers[_] th := sprintf(header_tpl, [header]) ] row := sprintf(row_tpl, [concat("", ths)]) } render_table_headers(headers) = "" { #if headers not specified return empty results count(headers) == 0 } render_table(headers, content_array) = s { rows := [tr | cells := content_array[_] tds := [td | ctext := cells[_] td := to_cell(ctext) ] tr = sprintf(row_tpl, [concat("", tds)]) ] s := sprintf(table_tpl, [concat("", array.concat([render_table_headers(headers)], rows))]) } ## why I added it? to_cell(txt) = c { c := sprintf(cell_tpl, [txt]) } to_colored_text(color, txt) = spn { spn := sprintf(colored_text_tpl, [color, txt]) } ####################################### Template specific functions ####################################### to_severity_color(color, level) = spn { spn := to_colored_text(color, format_int(with_default(input.Metadata.vulnerability_summary, level, 0), 10)) } cnt_by_severity(severity) = cnt { vln_list := [r | some i, j item := input.Results[i] item.Vulnerabilities[j].Severity == severity r := item.Vulnerabilities[j] ] cnt := count(vln_list) } # 2 dimension array for vulnerabilities summary severities_stats := [ ["critical", to_severity_color("#c00000", "critical")], ["high", to_severity_color("#e0443d", "high")], ["medium", to_severity_color("#f79421", "medium")], ["low", to_severity_color("#e1c930", "low")], ["unknown", to_severity_color("green", "unknown")], ] vlnrb_headers := ["Layer", "Title","Vulnerability ID", "Resource name", "Path", "Installed version", "Fix version", "Url"] render_vlnrb(severity, list) = sprintf(vlnrb_tpl, [severity, render_table(vlnrb_headers, list)]) { count(list) > 0 } render_vlnrb(severity, list) = "" { #returns empty string if list of vulnerabilities is passed count(list) == 0 } # builds 2-dimension array for vulnerability table vln_list(severity) = vlnrb { some i, j vlnrb := [r | item := input.Results[i] target := item.Target vlnname := item.Vulnerabilities[j].VulnerabilityID title := item.Vulnerabilities[j].Title fxvrsn := with_default(item.Vulnerabilities[j], "FixedVersion", "none") resource_name = with_default(item.Vulnerabilities[j], "PkgName", "none") resource_path = with_default(item.Vulnerabilities[j], "PkgPath", "none") resource_version = with_default(item.Vulnerabilities[j], "InstalledVersion", "none") primaryurl = with_default(item.Vulnerabilities[j], "PrimaryURL", "none") references = with_default(item.Vulnerabilities[j], "References", "none") item.Vulnerabilities[j].Severity == severity # only items with severity matched r := [target, title, vlnname, resource_name, resource_path, resource_version, fxvrsn, primaryurl] ] } ########################################################################################################### title = sprintf("%s vulnerability scan report", [input.ArtifactName]) aggregation_pkg := "postee.vuls.slack.trivy.aggregation" result = msg { msg := sprintf(tpl, [ input.ArtifactName, render_vlnrb("Critical", vln_list("CRITICAL")), render_vlnrb("High", vln_list("HIGH")), render_vlnrb("Medium", vln_list("MEDIUM")), render_vlnrb("Low", vln_list("LOW")), render_vlnrb("Unknown", vln_list("UNKNOWN")) ]) } ================================================ FILE: rego-templates/trivy-operator-dependency-track.rego ================================================ package postee.trivyoperator.dependencytrack title:=sprintf("%s:%s", [input.report.artifact.repository, input.report.artifact.tag]) result:=input.report.components ================================================ FILE: rego-templates/trivy-operator-jira.rego ================================================ package postee.trivyoperator.jira import data.postee.with_default ################################################ Templates ################################################ # main template to render message tpl:=` h1. Image: %s in namespace %s %s %s %s %s %s %s ` sum_tpl := ` h4. Summary totals: |critical: %s|high: %s|medium: %s|low: %s|unknown: %s| ` vlnrb_tpl = ` h4. %s severity vulnerabilities %s ` #Extra % is required in width:100% table_tpl := ` %s ` cell_tpl := `| %s ` header_tpl := `|| %s ` row_tpl_head := ` %s ||` row_tpl := ` %s |` colored_text_tpl := "{color:%s}%s{color}" ############################################## Html rendering ############################################# render_table_headers(headers) = row { count(headers) > 0 ths := [th | header := headers[_] th := sprintf(header_tpl, [header]) ] row := sprintf(row_tpl_head, [concat("", ths)]) } render_table_headers(headers) = "" { #if headers not specified return empty results count(headers) == 0 } render_table(headers, content_array) = s { rows := [tr | cells := content_array[_] tds := [td | ctext := cells[_] td := to_cell(ctext) ] tr = sprintf(row_tpl, [concat("", tds)]) ] s := sprintf(table_tpl, [concat("", array.concat([render_table_headers(headers)], rows))]) } ## why I added it? to_cell(txt) = c { c := sprintf(cell_tpl, [txt]) } to_colored_text(color, txt) = spn { spn := sprintf(colored_text_tpl, [color, txt]) } ####################################### Template specific functions ####################################### to_severity_color(color, level) = spn { spn := to_colored_text(color, format_int(with_default(input.report.summary,level, 0), 10)) } render_image_name := sprintf("%s:%s", [ with_default(input.report.artifact,"repository","unknown"), with_default(input.report.artifact,"tag","unknown") ]) render_summary := sprintf(sum_tpl,[ to_severity_color("#c00000", "criticalCount"), to_severity_color("#e0443d", "highCount"), to_severity_color("#f79421", "mediumCount"), to_severity_color("#e1c930", "lowCount"), to_severity_color("#505f79", "unknownCount") ]) vlnrb_headers := ["ID","Title", "Resource", "Installed version", "Fixed version", "Url"] render_vlnrb(severity, list) = sprintf(vlnrb_tpl, [severity, render_table(vlnrb_headers, list)]) { count(list) > 0 } render_vlnrb(severity, list) = "" { #returns empty string if list of vulnerabilities is passed count(list) == 0 } # builds 2-dimension array for vulnerability table vln_list(severity) = vlnrb { some j vlnrb := [r | item := input.report.vulnerabilities[j] vlnname := item.vulnerabilityID title := item.title fxvrsn := with_default(item, "fixedVersion", "none") resource = with_default(item, "resource", "none") resource_version = with_default(item, "installedVersion", "none") primaryurl = with_default(item, "primaryLink", "none") item.severity == severity # only items with severity matched r := [vlnname, title, resource, resource_version, fxvrsn, primaryurl] ] } ########################################################################################################### title = sprintf("Vulnerability issue with image %s in namespace %s", [render_image_name, with_default(input.metadata,"namespace","unknown")]) result = msg { msg := sprintf(tpl, [ render_image_name, with_default(input.metadata,"namespace","unknown"), render_summary, render_vlnrb("Critical", vln_list("CRITICAL")), render_vlnrb("High", vln_list("HIGH")), render_vlnrb("Medium", vln_list("MEDIUM")), render_vlnrb("Low", vln_list("LOW")), render_vlnrb("Unknown", vln_list("UNKNOWN")) ]) } ================================================ FILE: rego-templates/trivy-operator-slack.rego ================================================ package postee.trivyoperator.slack import data.postee.flat_array #converts [[{...},{...}], [{...},{...}]] to [{...},{...},{...},{...}] import data.postee.with_default ############################################# Common functions ############################################ # render_sections split collection of cells provided to chunks of 5 rows each and wraps every chunk with section element render_sections(rows, caption, headers) = result { count(rows) > 0 # only if some vulnerabilities are found rows_and_header := array.concat(headers, rows) a := flat_array([s | # code below converts 2 dimension array like [[row1, row2, ... row5], ....] group_size := 10 #it's 5 but every row is represented by 2 items num_chunks := ceil(count(rows_and_header) / group_size) - 1 indices := {b | b := numbers.range(0, num_chunks)[_] * group_size} some k fields := [array.slice(rows_and_header, i, i + group_size) | i := indices[_]][k] # builds markdown section based on slice s := with_caption(fields, caption, k) ]) result := array.concat(a, [{"type": "divider"}]) } render_sections(rows, caption, headers) = [] { #do not render section if provided collection is empty count(rows) == 0 } with_caption(fields, caption, position) = s { position == 0 s := [ { "type": "section", "text": { "type": "mrkdwn", "text": caption, }, }, { "type": "section", "fields": fields, }, ] } with_caption(fields, caption, position) = s { position > 0 s := [ { "type": "section", "fields": fields, }, ] } ########################################################################################################### vln_list(severity) = l { # builds list of rows for section for the given severity some i vlnrb := [r | item := input.report.vulnerabilities[i] vlnname := item.vulnerabilityID fxvrsn := with_default(item, "fixedVersion", "none") resource_name = with_default(item, "resource", "none") resource_version = with_default(item, "installedVersion", "none") url = with_default(item, "primaryLink","") item.severity == severity # only items with severity matched r := [ {"type": "mrkdwn", "text": sprintf("<%s|%s>",[url,vlnname])}, {"type": "mrkdwn", "text": concat(" / ", [resource_name, resource_version, fxvrsn])}, ] ] caption := sprintf("*%s severity vulnerabilities*", [severity]) #TODO make first char uppercase headers := [ {"type": "mrkdwn", "text": "*Vulnerability ID*"}, {"type": "mrkdwn", "text": "*Resource / Version / Fixed version*"}, ] # split rows and wrap slices with markdown section l := render_sections(flat_array(vlnrb), caption, headers) } image_name := sprintf("%s:%s", [ with_default(input.report.artifact,"repository","unknown"), with_default(input.report.artifact,"tag","unknown") ]) ########################################################################################################### postee := with_default(input, "postee", {}) title = sprintf("Vulnerability scan report %s", [image_name]) # title is result = res { header := [ { "type": "header", "text": { "type": "plain_text", "text": sprintf("Vulnerability issue with image:%s in namespace %s",[image_name, with_default(input.metadata,"namespace","unknown")]), }, } ] summary := [ { "type": "divider" }, { "type": "context", "elements": [ {"type": "mrkdwn", "text": "*Summary totals:*"}, ], }, { "type": "context", "elements": [ {"type": "mrkdwn", "text": sprintf("Critical: *%d*", [input.report.summary.criticalCount])}, {"type": "mrkdwn", "text": sprintf("High: *%d*", [input.report.summary.highCount])}, {"type": "mrkdwn", "text": sprintf("Medium: *%d*", [input.report.summary.mediumCount])}, {"type": "mrkdwn", "text": sprintf("Low: *%d*", [input.report.summary.lowCount])}, {"type": "mrkdwn", "text": sprintf("Unknown: *%d*", [input.report.summary.unknownCount])}, ], }, { "type": "divider" } ] res := flat_array([ header, summary, vln_list("CRITICAL"), vln_list("HIGH"), vln_list("MEDIUM"), vln_list("LOW"), vln_list("UNKNOWN") ]) } ================================================ FILE: rego-templates/trivy-vulns-slack.rego ================================================ package postee.vuls.trivy.slack import data.postee.by_flag import data.postee.duplicate import data.postee.flat_array #converts [[{...},{...}], [{...},{...}]] to [{...},{...},{...},{...}] import data.postee.with_default ############################################# Common functions ############################################ # TODO support generic property check_failed(item) = false { not item.failed } check_failed(item) { item.failed } ########################################################################################################### # render_sections split collection of cells provided to chunks of 5 rows each and wraps every chunk with section element render_sections(rows, caption, headers) = a { count(rows) > 0 # only if some vulnerabilities are found rows_and_header := array.concat(headers, rows) a := flat_array([s | # code below converts 2 dimension array like [[row1, row2, ... row5], ....] group_size := 10 #it's 5 but every row is represented by 2 items num_chunks := ceil(count(rows) / group_size) - 1 indices := {b | b := numbers.range(0, num_chunks)[_] * group_size} some k fields := [array.slice(rows_and_header, i, i + group_size) | i := indices[_]][k] # builds markdown section based on slice s := with_caption(fields, caption, k) ]) } render_sections(rows, caption, headers) = [] { #do not render section if provided collection is empty count(rows) == 0 } with_caption(fields, caption, position) = s { position == 0 s := [ { "type": "section", "text": { "type": "mrkdwn", "text": caption, }, }, { "type": "section", "fields": fields, }, ] } with_caption(fields, caption, position) = s { position > 0 s := [ { "type": "section", "fields": fields, }, ] } ########################################################################################################### vln_list(severity) = l { # builds list of rows for section for the given severity vlnrb := [r | some i, j item := input.Results[i] vlnname := item.Vulnerabilities[j].VulnerabilityID fxvrsn := with_default(item.Vulnerabilities[j], "FixedVersion", "none") resource_name = with_default(item.Vulnerabilities[j], "PkgName", "none") resource_version = with_default(item.Vulnerabilities[j], "InstalledVersion", "none") item.Vulnerabilities[j].Severity == severity r := [ {"type": "mrkdwn", "text": vlnname}, {"type": "mrkdwn", "text": concat(" / ", [resource_name, resource_version, fxvrsn])}, ] ] caption := sprintf("*%s severity vulnerabilities*", [severity]) #TODO make first char uppercase headers := [ {"type": "mrkdwn", "text": "*Vulnerability ID*"}, {"type": "mrkdwn", "text": "*Resource name / Installed version / Fix version*"}, ] # split rows and wrap slices with markdown section l := render_sections(flat_array(vlnrb), caption, headers) } cnt_by_severity(severity) = cnt { vln_list := [r | some i, j item := input.Results[i] item.Vulnerabilities[j].Severity == severity r := item.Vulnerabilities[j] ] cnt := count(vln_list) } ########################################################################################################### postee := with_default(input, "postee", {}) aqua_server := with_default(postee, "AquaServer", "") title = sprintf("Vulnerability scan report", []) # title is aggregation_pkg := "postee.vuls.slack.trivy.aggregation" result = res { severities := ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"] headers := [ {"type": "section", "text": {"type": "mrkdwn", "text": sprintf("Artifact name: %s", [input.ArtifactName])}}, {"type": "section", "text": {"type": "mrkdwn", "text": sprintf("Type: %s", [input.ArtifactType])}}, { "type": "section", "text": { "type": "mrkdwn", "text": "*Found vulnerabilities*", }, }, ] summary:= [ { "type": "section", "text": { "type": "mrkdwn", "text": "*Found vulnerabilities*", }, }, ] res := flat_array([ headers, vln_list("CRITICAL"), vln_list("HIGH"), vln_list("MEDIUM"), vln_list("LOW"), vln_list("UNKNOWN"), summary, [{ "type": "section", "fields": [ {"type": "mrkdwn", "text": "Critical"}, {"type": "mrkdwn", "text": sprintf("*%d*", [cnt_by_severity("CRITICAL")])}, {"type": "mrkdwn", "text": "High"}, {"type": "mrkdwn", "text": sprintf("*%d*", [cnt_by_severity("HIGH")])}, {"type": "mrkdwn", "text": "Medium"}, {"type": "mrkdwn", "text": sprintf("*%d*", [cnt_by_severity("MEDIUM")])}, {"type": "mrkdwn", "text": "Low"}, {"type": "mrkdwn", "text": sprintf("*%d*", [cnt_by_severity("LOW")])}, {"type": "mrkdwn", "text": "Unknown"}, {"type": "mrkdwn", "text": sprintf("*%d*", [cnt_by_severity("UNKNOWN")])}, ], }], ]) } ================================================ FILE: rego-templates/trivy-vuls-slack-aggregation.rego ================================================ package postee.vuls.slack.trivy.aggregation import data.postee.flat_array title := "Vulnerability scan report" url := urlsResult { urls := [ scan | item:=input[i].PrimaryURL scan:=[item] ] urlsResult:= concat("\n", flat_array(urls)) } result := res { scans := [ scan | item:=input[i].Description #collection is expected scan:=array.concat([{"type":"section","text":{"type":"mrkdwn","text": input[i].title}}], item) ] res:= flat_array(scans) } ================================================ FILE: rego-templates/vuls-cyclonedx.rego ================================================ package postee.vuls.cyclondx import data.postee.with_default bom_tpl:=` %s ` component_tpl:=` %s %s %s %s ` vlnrb_tpl := ` %s %s %v %v %v %s %s %s %s ` vlnrb_lst_tpl := `%s` render_vlnrb(vlnrb_lst) = xml { l := [r | vlnrb := vlnrb_lst[_] vln_name := vlnrb.name nvd_url := vlnrb.nvd_url # description is skipped vln_severity := vlnrb.aqua_severity vln_method := vlnrb.aqua_scoring_system vln_vectors := vlnrb.aqua_vectors vln_score := vlnrb.aqua_score vln_solution := with_default(vlnrb, "solution", "No solution available") r := sprintf(vlnrb_tpl, [vln_name, nvd_url, vln_score, vln_score, vln_score, vln_severity, vln_method, vln_vectors, vln_solution]) ] xml := sprintf(vlnrb_lst_tpl, [concat("", l)]) } render_components := l { l := [r | item := input.resources[_] component := item.resource component_name := with_default(component, "name", "none") component_version := with_default(component, "version", "none") # nexus iq has db limit for license field component_license := substring(with_default(component, "license", "not provided"), 0, 32) vlnrb:=render_vlnrb(item.vulnerabilities) r := sprintf(component_tpl, [component_name, component_version, component_license, vlnrb]) ] } title := input.image result := sprintf(bom_tpl, [concat("",render_components)]) ================================================ FILE: rego-templates/vuls-html-aggregation.rego ================================================ package postee.vuls.html.aggregation import data.postee.flat_array title := "Vulnerability scan report" url := urlsResult { urls := [ scan | item:=input[i].url scan:=[item] ] urlsResult:= concat("\n", flat_array(urls)) } result := res { scans := [ scan | item:=input[i].description scan:=[sprintf("

%s

", [input[i].title]), item] ] res:= concat("\n", flat_array(scans)) } ================================================ FILE: rego-templates/vuls-html.rego ================================================ package postee.vuls.html import data.postee.by_flag import data.postee.with_default #import common.by_flag ################################################ Templates ################################################ #main template to render message tpl:=`

Image name: %s

Registry: %s

%s

%s

%s

%s

Assurance controls

%s %s %s %s %s %s %s ` vlnrb_tpl = `

%s severity vulnerabilities

%s ` #Extra % is required in width:100% table_tpl:=` %s
` cell_tpl:=`%s ` header_tpl:=`%s ` row_tpl:=` %s ` colored_text_tpl:="%s" ########################################################################################################### ############################################## Html rendering ############################################# render_table_headers(headers) = row { count(headers) > 0 ths := [th | header := headers[_] th := sprintf(header_tpl, [header]) ] row := sprintf(row_tpl, [concat("", ths)]) } render_table_headers(headers) = "" { #if headers not specified return empty results count(headers) == 0 } render_table(headers, content_array) = s { rows := [tr | cells:=content_array[_] tds:= [td | ctext:=cells[_] td := to_cell(ctext) ] tr=sprintf(row_tpl, [concat("", tds)]) ] s:=sprintf(table_tpl, [concat("", array.concat([render_table_headers(headers)],rows))]) } ## why I added it? to_cell(txt) = c { c:= sprintf(cell_tpl, [txt]) } to_colored_text(color, txt) = spn { spn :=sprintf(colored_text_tpl, [color, txt]) } ####################################### Template specific functions ####################################### to_severity_color(color, level) = spn { spn:=to_colored_text(color, format_int(with_default(input.vulnerability_summary,level,0), 10)) } # TODO refactor to support different properties check_failed(item) = false { not item.failed #Either absent or false } check_failed(item) = true { item.failed } # 2 dimension array for vulnerabilities summary severities_stats := [ ["critical", to_severity_color("#c00000", "critical")], ["high", to_severity_color("#e0443d", "high")], ["medium", to_severity_color("#f79421", "medium")], ["low", to_severity_color("#e1c930", "low")], ["negligible", to_severity_color("green", "negligible")] ] # 2 dimension array for assurance controls assurance_controls := [ control | item := input.image_assurance_results.checks_performed[i] control := [format_int(i+1, 10), item.control,item.policy_name, by_flag( "FAIL", "PASS", check_failed(item) ) ] ] vlnrb_headers := ["Vulnerability ID", "Resource name", "Installed version", "Fix version"] render_vlnrb(severity, list) = sprintf(vlnrb_tpl, [severity, render_table(vlnrb_headers, list)]) { count(list) > 0 } render_vlnrb(severity, list) = "" { #returns empty string if list of vulnerabilities is passed count(list) == 0 } # builds 2-dimension array for vulnerability table vln_list(severity) = vlnrb { some i, j vlnrb := [r | item := input.resources[i] resource := item.resource vlnname := item.vulnerabilities[j].name fxvrsn := with_default(item.vulnerabilities[j],"fix_version", "none") resource_name = with_default(resource, "name", "none") resource_version = with_default(resource, "version", "none") item.vulnerabilities[j].aqua_severity == severity # only items with severity matched r := [vlnname, resource_name, resource_version, fxvrsn] ] } ########################################################################################################### postee := with_default(input, "postee", {}) aqua_server := with_default(postee, "AquaServer", "") title = sprintf("%s vulnerability scan report", [input.image]) href := sprintf("%s%s/%s", [aqua_server, urlquery.encode(input.registry), urlquery.encode(input.image)]) text := sprintf("%s%s/%s", [aqua_server, input.registry, input.image]) url := by_flag("", href, aqua_server == "") aggregation_pkg := "postee.vuls.html.aggregation" result = msg { msg := sprintf(tpl, [ input.image, input.registry, by_flag( "Image is non-compliant", "Image is compliant", with_default(input.image_assurance_results, "disallowed", false) ), by_flag( "Malware found: Yes", "Malware found: No", input.scan_options.scan_malware #reflects current logic ), by_flag( "Sensitive data found: Yes", "Sensitive data found: No", input.scan_options.scan_sensitive_data #reflects current logic ), render_table([], severities_stats), render_table(["#","Control","Policy Name", "Status"], assurance_controls), render_vlnrb("Critical", vln_list("critical")), render_vlnrb("High", vln_list("high")), render_vlnrb("Medium", vln_list("medium")), render_vlnrb("Low", vln_list("low")), render_vlnrb("Negligible", vln_list("negligible")), by_flag( "", sprintf(`

See more: %s

`,[href, text]), #link aqua_server == "") ]) } ================================================ FILE: rego-templates/vuls-opsgenie.rego ================================================ package postee.vuls.opsgenie title = input.image result= { "description":sprintf("%s vulnerability scan report", [input.image]), "alias":input.image } ================================================ FILE: rego-templates/vuls-slack-aggregation.rego ================================================ package postee.vuls.slack.aggregation import data.postee.flat_array title := "Vulnerability scan report" url := urlsResult { urls := [ scan | item:=input[i].url scan:=[item] ] urlsResult:= concat("\n", flat_array(urls)) } result := res { scans := [ scan | item:=input[i].description #collection is expected scan:=array.concat([{"type":"section","text":{"type":"mrkdwn","text": input[i].title}}], item) ] res:= flat_array(scans) } ================================================ FILE: rego-templates/vuls-slack.rego ================================================ package postee.vuls.slack import data.postee.by_flag import data.postee.flat_array #converts [[{...},{...}], [{...},{...}]] to [{...},{...},{...},{...}] import data.postee.duplicate import data.postee.with_default ############################################# Common functions ############################################ # TODO support generic property check_failed(item) = false { not item.failed } check_failed(item) = true { item.failed } ########################################################################################################### # render_sections split collection of cells provided to chunks of 5 rows each and wraps every chunk with section element render_sections(rows, caption) = a { count(rows) > 0 # only if some vulnerabilities are found a:=flat_array([ s | # code below converts 2 dimension array like [[row1, row2, ... row5], ....] group_size := 10 #it's 5 but every row is represented by 2 items num_chunks := ceil(count(rows) / group_size) - 1 indices := { b | b := numbers.range(0, num_chunks)[_] * group_size } fields:=[array.slice(rows, i, i + group_size) | i := indices[_]][_] # builds markdown section based on slice s := [ { "type": "section", "text": { "type": "mrkdwn", "text": caption } }, { "type": "section", "fields":fields } ] ]) } render_sections(rows, caption) = [] { #do not render section if provided collection is empty count(rows) == 0 } ########################################################################################################### vln_list(severity) = l { # builds list of rows for section for the given severity vlnrb := [r | some i, j item := input.resources[i] resource := item.resource vlnname := item.vulnerabilities[j].name fxvrsn := with_default(item.vulnerabilities[j],"fix_version", "none") resource_name = with_default(resource, "name", "none") resource_version = with_default(resource, "version", "none") item.vulnerabilities[j].aqua_severity == severity r := [ {"type": "mrkdwn", "text": vlnname}, {"type": "mrkdwn", "text": concat("/", [resource_name, resource_version, fxvrsn])} ] ] caption := sprintf("*%s severity vulnerabilities*", [severity]) #TODO make first char uppercase headers := [ {"type": "mrkdwn", "text": "*Vulnerability ID*"}, {"type": "mrkdwn", "text": "*Resource name / Installed version / Fix version*"} ] rows := array.concat(headers, flat_array(vlnrb)) # split rows and wrap slices with markdown section l := render_sections(rows, caption) } malware_list := l { mlwr := [r | item := input.malware[i] r := [ {"type": "mrkdwn", "text": sprintf("%d %s", [i+1, item.malware])}, {"type": "mrkdwn", "text": concat("/", [item.hash, item.path])} ] ] headers := [ {"type": "mrkdwn", "text": "*# Malware*"}, {"type": "mrkdwn", "text": "*Hash / Path*"} ] rows := array.concat(headers, flat_array(mlwr)) # split rows and wrap slices with markdown section l := render_sections(rows, "Malware") } ########################################################################################################### postee := with_default(input, "postee", {}) aqua_server := with_default(postee, "AquaServer", "") title = sprintf("%s vulnerability scan report", [input.image]) # title is string href:=sprintf("%s%s/%s", [aqua_server, urlquery.encode(input.registry), urlquery.encode(input.image)]) text:=sprintf("%s%s/%s", [aqua_server, input.registry, input.image]) url := by_flag("", href, aqua_server == "") aggregation_pkg := "postee.vuls.slack.aggregation" result = res { severities := ["critical", "high", "medium", "low", "negligible"] checks_performed:= flat_array([check | item := input.image_assurance_results.checks_performed[i] check:= [ {"type": "mrkdwn", "text": sprintf("%d %s", [i+1, item.control])}, {"type": "mrkdwn", "text": concat(" / ", [item.policy_name, by_flag("FAIL", "PASS", check_failed(item))])} ] ]) severity_stats:= flat_array([gr | severity := severities[_] gr:= [ {"type": "mrkdwn", "text": sprintf("*%s*", [upper(severity)])}, {"type": "mrkdwn", "text": sprintf("*%d*", [input.vulnerability_summary[severity]])}, ] ]) headers := [{"type":"section","text":{"type":"mrkdwn","text":sprintf("Image name: %s", [input.image])}}, {"type":"section","text":{"type":"mrkdwn","text":sprintf("Registry: %s", [input.registry])}}, {"type":"section","text":{"type":"mrkdwn","text": by_flag( "Image is non-compliant", "Image is compliant", with_default(input.image_assurance_results, "disallowed", false) )}}, {"type":"section","text":{"type":"mrkdwn","text": by_flag( "Malware found: Yes", "Malware found: No", input.scan_options.scan_malware #reflects current logic )}}, {"type":"section","text":{"type":"mrkdwn","text": by_flag( "Sensitive data found: Yes", "Sensitive data found: No", input.scan_options.scan_sensitive_data #reflects current logic )}}, { "type": "section", "fields": severity_stats }, { "type": "section", "text": { "type": "mrkdwn", "text": "*Assurance controls*" } }, { "type": "section", "fields": array.concat( [{ "type": "mrkdwn", "text": "*#* *Control*" }, { "type": "mrkdwn", "text": "*Policy Name* / *Status*" }], checks_performed) }, { "type": "section", "text": { "type": "mrkdwn", "text": "*Found vulnerabilities*" } } ] urlText :=sprintf("See more: \u003c%s|%s\u003e", [href, text]) footers := by_flag( "", [{ "type": "section", "text": { "type": "mrkdwn", "text": urlText } }], aqua_server == "") res := flat_array([ headers, vln_list("critical"), vln_list("high"), vln_list("medium"), vln_list("low"), vln_list("negligible"), malware_list, footers ]) } ================================================ FILE: regoservice/aggregation_test.go ================================================ package regoservice import ( "io/ioutil" "os" "testing" ) var ( regoWithAggregation = ` package rego1 title:="Audit event received" result:=sprintf("Audit event received from %s", [input.user]) aggregation_pkg:="rego1.aggr" ` aggregationRego = ` package rego1.aggr import data.postee.flat_array title := "Vulnerability scan report" result := res { scans := [ scan | item:=input[i].description scan:=[sprintf("

%s

", [input[i].title]), item] ] res:= concat("\n", flat_array(scans)) } ` commonRego = `package postee flat_array(a) = o { o:=[item | item:=a[_][_] ] } ` ) func TestAggregation(t *testing.T) { tests := []struct { regoRule *string aggregationRegoRule *string caseDesc string items []map[string]string regoPackage string expectedValues map[string]string }{ { regoRule: ®oWithAggregation, aggregationRegoRule: &aggregationRego, caseDesc: "simple case", items: []map[string]string{{ "title": "title1", "description": "description1", }, { "title": "title2", "description": "description2", }}, regoPackage: "rego1", expectedValues: map[string]string{ "title": "Vulnerability scan report", "description": `

title1

description1

title2

description2`, }, }, } for _, test := range tests { aggregateBuildinRego(t, test.regoRule, test.aggregationRegoRule, test.items, test.regoPackage, test.expectedValues) } } func aggregateBuildinRego(t *testing.T, regoRule *string, aggregationRegoRule *string, items []map[string]string, regoPackage string, expectedValues map[string]string) { buildinRegoTemplatesSaved := buildinRegoTemplates testRego := "rego1.rego" aggrRego := "aggr1.rego" commonRegoFilename := "common.rego" buildinRegoTemplates = []string{commonRegoFilename, testRego, aggrRego} //common part goes in single bundle err := ioutil.WriteFile(commonRegoFilename, []byte(commonRego), 0644) err = ioutil.WriteFile(testRego, []byte(*regoRule), 0644) err = ioutil.WriteFile(aggrRego, []byte(*aggregationRegoRule), 0644) if err != nil { t.Fatal(err) } defer func() { buildinRegoTemplates = buildinRegoTemplatesSaved os.Remove(testRego) os.Remove(commonRegoFilename) os.Remove(aggrRego) }() demo, err := BuildBundledRegoEvaluator(regoPackage) if err != nil { t.Errorf("received an unexpected error: %v\n", err) } if !demo.IsAggregationSupported() { t.Errorf("Should support aggregation") return } r, err := demo.BuildAggregatedContent(items) if err != nil { t.Errorf("received an unexpected error: %v\n", err) } for key, expected := range expectedValues { if r[key] != expected { t.Errorf("Incorrect %s: expected %s, got %s\n", key, expected, r[key]) } } } ================================================ FILE: regoservice/eval.go ================================================ package regoservice import ( "bytes" "context" "encoding/json" "errors" "fmt" "github.com/aquasecurity/postee/v2/data" "github.com/open-policy-agent/opa/rego" "io/fs" "log" ) const ( result_prop = "result" title_prop = "title" url_prop = "url" aggregation_pkg_prop = "aggregation_pkg" //ServiceNow props dateProp = "result_date" severityProp = "result_severity" categoryProp = "result_category" subcategoryProp = "result_subcategory" assignedToProp = "result_assigned_to" assignedGroupProp = "result_assigned_group" summaryProp = "result_summary" ) var ( buildinRegoTemplates = []string{"./rego-templates"} commonRegoTemplates = []string{"./rego-templates/common"} ) type regoEvaluator struct { prepQuery *rego.PreparedEvalQuery aggrQuery *rego.PreparedEvalQuery isPackageDefined bool } func (regoEvaluator *regoEvaluator) IsAggregationSupported() bool { return regoEvaluator.aggrQuery != nil } func (regoEvaluator *regoEvaluator) Eval(in map[string]interface{}, serverUrl string) (map[string]string, error) { ctx := context.Background() rs, err := regoEvaluator.prepQuery.Eval(ctx, rego.EvalInput(in)) if err != nil { return nil, err } if len(rs) == 0 || len(rs[0].Expressions) == 0 { return nil, errors.New("no results") //TODO error definition } var expr interface{} if regoEvaluator.isPackageDefined { expr = rs[0].Expressions[0].Value } else { expr = getFirstElement(rs[0].Expressions[0].Value.(map[string]interface{}), result_prop) if expr == nil { return nil, errors.New("invalid rego template structure") } } data := expr.(map[string]interface{}) title, err := asStringOrJson(data, title_prop) if err != nil { return nil, err } description, err := asStringOrJson(data, result_prop) if err != nil { return nil, err } shortMessageUrl, ok := data[url_prop].(string) if !ok { shortMessageUrl = "" } // variables for servicenow // for other templates must be empty date := getStringFromData(data, dateProp) severity := getStringFromData(data, severityProp) category := getStringFromData(data, categoryProp) subcategory := getStringFromData(data, subcategoryProp) assignedTo := getStringFromData(data, assignedToProp) assignedGroup := getStringFromData(data, assignedGroupProp) summary := getStringFromData(data, summaryProp) return map[string]string{ "title": title, "description": description, "url": shortMessageUrl, "date": date, "severity": severity, "summary": summary, "category": category, "subcategory": subcategory, "assignedTo": assignedTo, "assignedGroup": assignedGroup, }, nil } func getStringFromData(data map[string]interface{}, prop string) string { value := "" v, ok := data[prop] if ok { switch v.(type) { case string: value = v.(string) case json.Number: value = v.(json.Number).String() } } return value } func getFirstElement(context map[string]interface{}, key string) interface{} { for _, v := range context { log.Printf("checking: %s ...\n", key) childCtx, ok := v.(map[string]interface{}) if !ok { return nil } if childCtx[key] != nil { return v } else { found := getFirstElement(childCtx, key) if found != nil { return found } } } return nil } func asStringOrJson(data map[string]interface{}, prop string) (string, error) { expr, ok := data[prop] if !ok { return "", errors.New(fmt.Sprintf("property %s is not found", prop)) } switch v := expr.(type) { // TODO: Use json.Valid() instead case string: return v, nil default: val, err := json.Marshal(expr) if err != nil { return "", err } var out bytes.Buffer if err = json.Compact(&out, val); err != nil { // Remove extra '\n' et al. return "", err } return out.String(), nil } } func (regoEvaluator *regoEvaluator) BuildAggregatedContent(scans []map[string]string) (map[string]string, error) { aggregatedJson := make([]map[string]interface{}, len(scans), len(scans)) for _, scan := range scans { desc := scan["description"] var in []map[string]interface{} item := make(map[string]interface{}) if err := json.Unmarshal([]byte(desc), &in); err != nil { item["description"] = desc //description is not json, so it's passed as string } else { item["description"] = in } item["title"] = scan["title"] item[url_prop] = scan[url_prop] // ServiceNow item["date"] = scan["date"] item["severity"] = scan["severity"] item["summary"] = scan["summary"] item["category"] = scan["category"] item["subcategory"] = scan["subcategory"] item["assignedTo"] = scan["assignedTo"] item["assignedGroup"] = scan["assignedGroup"] aggregatedJson = append(aggregatedJson, item) } ctx := context.Background() rs, err := regoEvaluator.aggrQuery.Eval(ctx, rego.EvalInput(aggregatedJson)) if err != nil { return nil, err } if len(rs) == 0 || len(rs[0].Expressions) == 0 { return nil, errors.New("no results") //TODO error definition } expr := rs[0].Expressions[0].Value data := expr.(map[string]interface{}) title, err := asStringOrJson(data, title_prop) if err != nil { return nil, err } description, err := asStringOrJson(data, result_prop) if err != nil { return nil, err } shortMessageUrl, ok := data[url_prop].(string) if !ok { shortMessageUrl = "" } // variables for servicenow // for other templates must be empty date := getStringFromData(data, dateProp) severity := getStringFromData(data, severityProp) category := getStringFromData(data, categoryProp) subcategory := getStringFromData(data, subcategoryProp) assignedTo := getStringFromData(data, assignedToProp) assignedGroup := getStringFromData(data, assignedGroupProp) summary := getStringFromData(data, summaryProp) return map[string]string{ "title": title, "description": description, "url": shortMessageUrl, "date": date, "severity": severity, "summary": summary, "category": category, "subcategory": subcategory, "assignedTo": assignedTo, "assignedGroup": assignedGroup, }, nil } func BuildBundledRegoEvaluator(rego_package string) (data.Inpteval, error) { r, err := buildBundledRegoForPackage(rego_package) if err != nil { return nil, err } aggrQuery, err := buildAggregatedRego(r) if err != nil { return nil, err } return ®oEvaluator{ prepQuery: r, isPackageDefined: true, aggrQuery: aggrQuery, }, nil } func buildBundledRegoForPackage(rego_package string) (*rego.PreparedEvalQuery, error) { ctx := context.Background() query := fmt.Sprintf("data.%s", rego_package) r, err := rego.New( rego.Query(query), jsonFmtFunc(), rego.Load(buildinRegoTemplates, filterRegoTemplateFiles), ).PrepareForEval(ctx) if err != nil { return nil, err } return &r, nil } // there is case when k8s creates `lost+found` file without access (bad permission) in template folder // skip this file to avoid error func filterRegoTemplateFiles(_ string, info fs.FileInfo, _ int) bool { if info.Name() == "lost+found" { return true } return false } func buildAggregatedRego(query *rego.PreparedEvalQuery) (*rego.PreparedEvalQuery, error) { ctx := context.Background() //execute query with empty input and check if aggregation package is defined rs, err := query.Eval(ctx, rego.EvalInput(make(map[string]interface{}))) if len(rs) == 0 || len(rs[0].Expressions) == 0 { return nil, errors.New("no results") //TODO error definition } expr := rs[0].Expressions[0].Value.(map[string]interface{}) aggregation_pkg_val := expr[aggregation_pkg_prop] var aggrQuery *rego.PreparedEvalQuery if aggregation_pkg_val != nil { aggregation_pkg := aggregation_pkg_val.(string) aggrQuery, err = buildBundledRegoForPackage(aggregation_pkg) if err != nil { return nil, err } } else { //it's ok skip aggregation package - no aggregation features will be available log.Printf("No aggregation package configured!!!") } return aggrQuery, nil } func BuildExternalRegoEvaluator(filename string, body string) (data.Inpteval, error) { ctx := context.Background() r, err := rego.New( rego.Query("data"), jsonFmtFunc(), rego.Load(commonRegoTemplates, filterRegoTemplateFiles), //only common modules rego.Module(filename, body), ).PrepareForEval(ctx) if err != nil { return nil, err } aggrQuery, err := buildAggregatedRego(&r) if err != nil { return nil, err } return ®oEvaluator{ prepQuery: &r, isPackageDefined: false, aggrQuery: aggrQuery, }, nil } ================================================ FILE: regoservice/eval_test.go ================================================ package regoservice import ( "encoding/json" "flag" "github.com/stretchr/testify/require" "io/fs" "os" "path/filepath" "testing" ) var update = flag.Bool("update", false, "update golden files") func TestEval(t *testing.T) { tests := []struct { regoRule *string templateFile string caseDesc string inputFile string regoPackage string expectedValues map[string]string // Description saves in golden file expectedDescriptionFile string shouldEvalFail bool shouldPrepareFail bool skipBuildin bool skipExternal bool }{ /* cases for basic functionality */ { caseDesc: "simple case producing html output", inputFile: "testdata/inputs/simple-input.json", templateFile: "testdata/templates/html.rego", regoPackage: "rego1.html", expectedDescriptionFile: "testdata/goldens/html.golden", expectedValues: map[string]string{ "title": "Audit event received", "url": "Audit-registry-received/Audit-image-received", }, }, { caseDesc: "Multilevel package", inputFile: "testdata/inputs/simple-input.json", templateFile: "testdata/templates/html-with-complex-pkg.rego", regoPackage: "rego2.html", expectedDescriptionFile: "testdata/goldens/html-with-complex-pkg.golden", expectedValues: map[string]string{ "title": "Audit event received", "url": "Audit-registry-received/Audit-image-received", }, }, { caseDesc: "producing json output", inputFile: "testdata/inputs/simple-input.json", templateFile: "testdata/templates/json.rego", regoPackage: "rego1.json", expectedDescriptionFile: "testdata/goldens/json.golden", expectedValues: map[string]string{ "title": "Audit event received", "url": "Audit-registry-received/Audit-image-received", }, }, { caseDesc: "producing json output without url", inputFile: "testdata/inputs/simple-input.json", templateFile: "testdata/templates/json-without-url.rego", regoPackage: "rego1.json.without.url", expectedDescriptionFile: "testdata/goldens/json-without-url.golden", expectedValues: map[string]string{ "title": "Audit event received", "url": "", }, }, /* cases for templates from `rego-templates` directory */ { caseDesc: "raw-message-html.rego template", inputFile: "testdata/inputs/simple-input.json", templateFile: "../rego-templates/raw-message-html.rego", regoPackage: "postee.rawmessage.html", expectedValues: map[string]string{ "title": "Raw Message Received", }, expectedDescriptionFile: "testdata/goldens/raw-message-html.golden", }, { caseDesc: "raw-message-json.rego template", inputFile: "testdata/inputs/simple-input.json", templateFile: "../rego-templates/raw-message-json.rego", regoPackage: "postee.rawmessage.json", expectedValues: map[string]string{ "title": "-", }, expectedDescriptionFile: "testdata/goldens/raw-message-json.golden", }, { caseDesc: "trivy-jira.rego template", inputFile: "testdata/inputs/trivy-input.json", templateFile: "../rego-templates/trivy-jira.rego", regoPackage: "postee.trivy.jira", expectedValues: map[string]string{ "title": "pom.xml vulnerability scan report", }, expectedDescriptionFile: "testdata/goldens/trivy-jira.golden", }, { caseDesc: "trivy-vulns-slack.rego template", inputFile: "testdata/inputs/trivy-input.json", templateFile: "../rego-templates/trivy-vulns-slack.rego", regoPackage: "postee.vuls.trivy.slack", expectedValues: map[string]string{ "title": "Vulnerability scan report", }, expectedDescriptionFile: "testdata/goldens/trivy-vulns-slack.golden", }, { caseDesc: "vuls-html.rego template", inputFile: "testdata/inputs/aqua-input.json", templateFile: "../rego-templates/vuls-html.rego", regoPackage: "postee.vuls.html", expectedValues: map[string]string{ "title": "all-in-one:3.5.19223 vulnerability scan report", }, expectedDescriptionFile: "testdata/goldens/vuls-html.golden", }, { caseDesc: "vuls-html.rego template", inputFile: "testdata/inputs/aqua-input.json", templateFile: "../rego-templates/vuls-html.rego", regoPackage: "postee.vuls.html", expectedValues: map[string]string{ "title": "all-in-one:3.5.19223 vulnerability scan report", }, expectedDescriptionFile: "testdata/goldens/vuls-html.golden", }, { caseDesc: "vuls-slack.rego template", inputFile: "testdata/inputs/aqua-input.json", templateFile: "../rego-templates/vuls-slack.rego", regoPackage: "postee.vuls.slack", expectedValues: map[string]string{ "title": "all-in-one:3.5.19223 vulnerability scan report", }, expectedDescriptionFile: "testdata/goldens/vuls-slack.golden", }, { caseDesc: "vuls-cyclonedx.rego template", inputFile: "testdata/inputs/aqua-input.json", templateFile: "../rego-templates/vuls-cyclonedx.rego", regoPackage: "postee.vuls.cyclondx", expectedValues: map[string]string{ "title": "all-in-one:3.5.19223", }, expectedDescriptionFile: "testdata/goldens/vuls-cyclonedx.golden", }, { caseDesc: "servicenow.rego template", inputFile: "testdata/inputs/aqua-input.json", templateFile: "../rego-templates/servicenow.rego", regoPackage: "postee.servicenow", expectedValues: map[string]string{ "title": "Aqua security | image | all-in-one:3.5.19223 | Scan report", "category": "Security Image Scan results", "subcategory": "Security incident", "date": "1624544066", "severity": "1", "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", "assignedTo": "owner", "assignedGroup": "group", }, expectedDescriptionFile: "testdata/goldens/servicenow.golden", }, { caseDesc: "servicenow-incident.rego template", inputFile: "testdata/inputs/aqua-incident-input.json", templateFile: "../rego-templates/servicenow-incident.rego", regoPackage: "postee.servicenow.incident", expectedValues: map[string]string{ "title": "test", "category": "Security incident", "severity": "3", "summary": "Category: Test\nSeverity: 3", }, expectedDescriptionFile: "testdata/goldens/servicenow-incident.golden", }, { caseDesc: "servicenow-insight.rego template", inputFile: "testdata/inputs/aqua-insight-input.json", templateFile: "../rego-templates/servicenow-insight.rego", regoPackage: "postee.servicenow.insight", expectedValues: map[string]string{ "title": "Workloads or images containing login data", "category": "Security insight", "severity": "2", "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: ", }, expectedDescriptionFile: "testdata/goldens/servicenow-insight.golden", }, { caseDesc: "trivy-operator-jira.rego template", inputFile: "testdata/inputs/trivy-operator-input.json", templateFile: "../rego-templates/trivy-operator-jira.rego", regoPackage: "postee.trivyoperator.jira", expectedValues: map[string]string{ "title": "Vulnerability issue with image library/nginx:1.16 in namespace default", }, expectedDescriptionFile: "testdata/goldens/trivy-operator-jira.golden", }, { caseDesc: "trivy-operator-slack.rego template", inputFile: "testdata/inputs/trivy-operator-input.json", templateFile: "../rego-templates/trivy-operator-slack.rego", regoPackage: "postee.trivyoperator.slack", expectedValues: map[string]string{ "title": "Vulnerability scan report library/nginx:1.16", }, expectedDescriptionFile: "testdata/goldens/trivy-operator-slack.golden", }, /* cases which should fail are below*/ { caseDesc: "Rego with wrong package specified", inputFile: "testdata/inputs/simple-input.json", templateFile: "testdata/templates/without-result.rego", regoPackage: "rego3", expectedValues: map[string]string{}, shouldPrepareFail: true, skipExternal: true, }, { caseDesc: "Rego without any expression", inputFile: "testdata/inputs/simple-input.json", templateFile: "testdata/templates/without-any-expression.rego", regoPackage: "rego1.without.any.expression", shouldEvalFail: true, }, { caseDesc: "Invalid Rego", inputFile: "testdata/inputs/simple-input.json", templateFile: "testdata/templates/invalid.rego", regoPackage: "rego1.invalid", expectedValues: map[string]string{}, shouldEvalFail: true, }, } for _, test := range tests { t.Run(test.caseDesc, func(t *testing.T) { if !test.skipBuildin { evaluateBuildinRego(t, test.inputFile, test.templateFile, test.expectedDescriptionFile, test.regoPackage, test.expectedValues, test.shouldEvalFail, test.shouldPrepareFail) } if !test.skipExternal { evaluateExternalRego(t, test.inputFile, test.templateFile, test.expectedDescriptionFile, test.expectedValues, test.shouldEvalFail, test.shouldPrepareFail) } }) } } func evaluateBuildinRego(t *testing.T, inputFile, templateFile, descriptionGoldenFile, regoPackage string, expectedValues map[string]string, shouldEvalFail bool, shouldPrepareFail bool) { buildinRegoTemplatesSaved := buildinRegoTemplates buildinRegoTemplates = []string{filepath.Dir(templateFile)} defer func() { buildinRegoTemplates = buildinRegoTemplatesSaved }() demo, err := BuildBundledRegoEvaluator(regoPackage) if shouldPrepareFail { require.Error(t, err, "test case should fail on prepare") return } require.NoError(t, err) f, err := os.Open(inputFile) require.NoError(t, err) defer f.Close() in := make(map[string]interface{}) err = json.NewDecoder(f).Decode(&in) require.NoError(t, err) r, err := demo.Eval(in, "") if shouldEvalFail { require.Error(t, err, "test case should fail on eval") return } require.NoError(t, err) // write description in file descriptionFile := filepath.Join(t.TempDir(), "description.txt") if *update { descriptionFile = descriptionGoldenFile } err = os.WriteFile(descriptionFile, []byte(r["description"]), 0644) require.NoError(t, err) compareDescriptions(t, descriptionGoldenFile, descriptionFile) for key, expected := range expectedValues { want := r[key] require.EqualValues(t, expected, want) } } func evaluateExternalRego(t *testing.T, inputFile, templateFile, descriptionGoldenFile string, expectedValues map[string]string, shouldEvalFail bool, shouldPrepareFail bool) { commonRegoTemplatesSaved := commonRegoTemplates commonRegoDir := filepath.Join(filepath.Dir(templateFile), "common", "common.rego") commonRegoTemplates = []string{commonRegoDir} defer func() { commonRegoTemplates = commonRegoTemplatesSaved }() b, err := os.ReadFile(templateFile) require.NoError(t, err) demo, err := BuildExternalRegoEvaluator(templateFile, string(b)) if shouldPrepareFail { require.Error(t, err, "test case should fail on prepare") return } require.NoError(t, err) f, err := os.Open(inputFile) require.NoError(t, err) defer f.Close() in := make(map[string]interface{}) err = json.NewDecoder(f).Decode(&in) require.NoError(t, err) r, err := demo.Eval(in, "") if shouldEvalFail { require.Error(t, err, "test case should fail on eval") return } require.NoError(t, err) // write description in file descriptionFile := filepath.Join(t.TempDir(), "description.txt") if *update { descriptionFile = descriptionGoldenFile } err = os.WriteFile(descriptionFile, []byte(r["description"]), 0644) require.NoError(t, err) compareDescriptions(t, descriptionGoldenFile, descriptionFile) for key, expected := range expectedValues { want := r[key] require.EqualValues(t, expected, want) } } func compareDescriptions(t *testing.T, expectedFile, gotFile string) { expected, err := os.ReadFile(expectedFile) require.NoError(t, err) got, err := os.ReadFile(gotFile) require.NoError(t, err) require.Equal(t, string(expected), string(got)) } func TestBuildBundledRegoForPackage(t *testing.T) { regoRule := ` package rego1 title:="Audit event received" result:=sprintf("Audit event received from %s", [input.user]) url:="Audit-registry-received/Audit-image-received" ` tests := []struct { name string fileName string perm fs.FileMode wantRules bool wantErr string }{ { name: "happy path", fileName: "rego1.rego", perm: 0644, wantRules: true, }, { name: "bad permission", fileName: "rego1.rego", perm: 0000, wantErr: "permission denied", }, { name: "lost+found", fileName: "lost+found", perm: 0644, }, { name: "lost+found with bad permission", fileName: "lost+found", perm: 0000, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { regoFilePath := filepath.Join(t.TempDir(), tt.fileName) err := os.WriteFile(regoFilePath, []byte(regoRule), tt.perm) require.NoError(t, err) savedBuildinRegoTemplates := buildinRegoTemplates buildinRegoTemplates = []string{regoFilePath} defer func() { buildinRegoTemplates = savedBuildinRegoTemplates }() r, err := buildBundledRegoForPackage("rego1") if tt.wantErr != "" { require.ErrorContains(t, err, tt.wantErr) return } if tt.wantRules { require.NotEmpty(t, r.Modules()) return } require.Empty(t, r.Modules()) }) } } ================================================ FILE: regoservice/jsonformat.go ================================================ package regoservice import ( "encoding/json" "log" "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/rego" "github.com/open-policy-agent/opa/types" ) func jsonFmtFunc() func(r *rego.Rego) { return rego.Function1( ®o.Function{ Name: "jsonformat", Decl: types.NewFunction(types.Args(&types.Object{}), types.S), }, func(_ rego.BuiltinContext, a *ast.Term) (*ast.Term, error) { obj := make(map[string]interface{}) err := ast.As(a.Value, &obj) if err != nil { //Rego doesn't show errors log.Printf("Can't convert OPA object: %v\n", err) return nil, err } b, err := json.MarshalIndent(obj, "", " ") if err != nil { //Rego doesn't show errors log.Printf("Error while json format: %v\n", err) return nil, err } return ast.StringTerm(string(b)), nil }) } ================================================ FILE: regoservice/regocheck.go ================================================ package regoservice import ( "context" "fmt" "os" "path/filepath" "strings" "github.com/open-policy-agent/opa/rego" ) const ( module = `package postee default allow = false allow { %s } ` defaultPathToRegoFilters = "./rego-filters" ) var pathToRegoFilters = "" func getFilesWithPathToRegoFilters(files []string) []string { if pathToRegoFilters == "" { if os.Getenv("REGO_FILTERS_PATH") != "" { pathToRegoFilters = os.Getenv("REGO_FILTERS_PATH") } else { pathToRegoFilters = defaultPathToRegoFilters } } filesWithPath := make([]string, len(files)) copy(filesWithPath, files) for i, file := range filesWithPath { if !strings.HasPrefix(file, pathToRegoFilters) { filesWithPath[i] = filepath.Join(pathToRegoFilters, file) } } return filesWithPath } func buildRegoLoader(files []string, rule string) func(r *rego.Rego) { if IsUsedRegoFiles(files) { filesWithPath := getFilesWithPathToRegoFilters(files) return rego.Load(filesWithPath, nil) } return rego.Module("postee.rego", fmt.Sprintf(module, rule)) } func IsUsedRegoFiles(files []string) bool { return len(files) != 0 && files[0] != "" } func DoesMatchRegoCriteria(input interface{}, files []string, rule string) (bool, error) { if !IsUsedRegoFiles(files) && rule == "" { return true, nil } ctx := context.Background() r := rego.New( rego.Query("x = data.postee.allow"), buildRegoLoader(files, rule), ) query, err := r.PrepareForEval(ctx) if err != nil { return false, err } rs, err := query.Eval(ctx, rego.EvalInput(input)) if err != nil { return false, err } if len(rs) > 0 { switch rs[0].Bindings["x"].(type) { case bool: return rs[0].Bindings["x"].(bool), nil } } return false, nil } ================================================ FILE: regoservice/regocheck_test.go ================================================ package regoservice import ( "encoding/json" "fmt" "os" "testing" ) func TestOpaRego(t *testing.T) { rego := `contains(input.image, "alpine")` incorrectRego := `default input = false` emptyRego := "" correctInputFiles := []string{"../correctInputFiles.rego"} incorrectInputFiles := []string{"../incorrectInputFiles.rego"} emptyinputFiles := []string{} correctFile, err := os.Create("correctInputFiles.rego") if err != nil { t.Errorf("error create file: %v", err) } _, err = correctFile.WriteString(fmt.Sprintf(module, rego)) if err != nil { t.Fatal(err) } defer os.Remove("correctInputFiles.rego") defer correctFile.Close() incorrectFile, err := os.Create("incorrectInputFiles.rego") if err != nil { t.Errorf("error create file: %v", err) } _, err = incorrectFile.WriteString(fmt.Sprintf(module, incorrectRego)) if err != nil { t.Errorf("error create file: %v", err) } defer os.Remove("incorrectInputFiles.rego") defer incorrectFile.Close() scanResult := `{"image":"alpine:26"}` scanNoJson := "simple text" scanWithoutResult := `{"image":"1science:latest"}` tests := []struct { rules string inputFiles []string scan string result bool shouldTriggerError bool }{ {rego, emptyinputFiles, scanResult, true, false}, {rego, emptyinputFiles, scanNoJson, false, true}, {rego, emptyinputFiles, scanWithoutResult, false, false}, {emptyRego, correctInputFiles, scanResult, true, false}, {emptyRego, incorrectInputFiles, scanNoJson, false, true}, {emptyRego, emptyinputFiles, scanWithoutResult, true, false}, {incorrectRego, emptyinputFiles, scanResult, false, true}, {emptyRego, emptyinputFiles, scanResult, true, false}, } for _, test := range tests { intr := map[string]interface{}{} if err := json.Unmarshal([]byte(test.scan), &intr); err != nil && !test.shouldTriggerError { t.Errorf("json.Unmarshal(%q) error: %v", test.scan, err) continue } got, err := DoesMatchRegoCriteria(intr, test.inputFiles, test.rules) if err != nil && !test.shouldTriggerError { t.Errorf("received an unexpected error: %v", err) continue } if got != test.result { t.Errorf("DoesMatchRegoCriteria(%q, %q, %q) == %t, wanted %t", test.scan, test.inputFiles, test.rules, got, test.result) } } } func TestGetFilesWithPathToRegoFilters(t *testing.T) { oldEnv := os.Getenv("REGO_FILTERS_PATH") defer os.Setenv("REGO_FILTERS_PATH", oldEnv) oldPathToRegoFilters := pathToRegoFilters tests := []struct { files []string env string expectedfiles []string }{ {[]string{"policy.rego", "ignore.rego"}, "", []string{"rego-filters/policy.rego", "rego-filters/ignore.rego"}}, {[]string{"policy.rego", "ignore.rego"}, "filters", []string{"filters/policy.rego", "filters/ignore.rego"}}, {[]string{"policy.rego", "ignore.rego"}, "filters/regofiles", []string{"filters/regofiles/policy.rego", "filters/regofiles/ignore.rego"}}, {[]string{"policy.rego", "ignore.rego"}, "/filters/regofiles", []string{"/filters/regofiles/policy.rego", "/filters/regofiles/ignore.rego"}}, {[]string{}, "./rego", []string{}}, } for _, test := range tests { pathToRegoFilters = "" os.Setenv("REGO_FILTERS_PATH", test.env) fmt.Println(pathToRegoFilters) filesWithPath := getFilesWithPathToRegoFilters(test.files) for i := range test.expectedfiles { if test.expectedfiles[i] != filesWithPath[i] { t.Errorf("Error for env: %s\n expected file: %s, got: %s", test.env, test.expectedfiles[i], filesWithPath[i]) } } } pathToRegoFilters = oldPathToRegoFilters } ================================================ FILE: regoservice/testdata/goldens/html-with-complex-pkg.golden ================================================ Audit event received from demo ================================================ FILE: regoservice/testdata/goldens/html.golden ================================================ Audit event received from demo ================================================ FILE: regoservice/testdata/goldens/json-without-url.golden ================================================ {"assignee":"demo"} ================================================ FILE: regoservice/testdata/goldens/json.golden ================================================ {"assignee":"demo"} ================================================ FILE: regoservice/testdata/goldens/raw-message-html.golden ================================================
{
 "user": "demo"
}
================================================ FILE: regoservice/testdata/goldens/raw-message-json.golden ================================================ { "user": "demo" } ================================================ FILE: regoservice/testdata/goldens/servicenow-incident.golden ================================================

Name: test

Category: Test

Severity: 3

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

Resourse policy name: test

Resourse policy application scopes: ["scope1", "scope2"]

================================================ FILE: regoservice/testdata/goldens/servicenow-insight.golden ================================================

Insight Details:

Insight ID: aqua-3006

Description: Workloads or images containing login data

Impact: Attackers with access to this workload or image might be able to use the login data to gain initial access to other resources

Severity: medium

Found Date: 2022-08-25T09:02:28.991Z

Last Scan: 2022-08-25T08:59:42.314673Z

URL:

Resourse Details:

Resourse ID: 6131180

Resourse Name: shayyo/sensitive_data:latest

ARN:

Extra Info: {"Image": "sensitive_data:latest", "Registry": "Docker Hub", "RegistryDomain": "docker.io"}

Evidence:

Sensitive data

File Type File Path Image
RSA PRIVATE KEY /private-key.pem shayyo/sensitive_data:latest

Recommendation:

No Recommendation

Resourse policy name: insights_login data

Resourse policy application scopes: ["scope1", "scope2"]

================================================ FILE: regoservice/testdata/goldens/servicenow.golden ================================================

Name: all-in-one:3.5.19223

Registry: Aqua

Malware found: Yes

Sensitive data found: Yes

Vulnerability summary

critical 1
high 1
medium 1
low 1
negligible 1

Assurance controls

# Control Policy Name Status
1 malware Default PASS
2 license Default PASS
3 max_severity Default PASS

Critical severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2018-1000517 busybox 1.28.4-r3 1.29.0

High severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2021-33560 libgcrypt 1.8.3-r0 1.8.8

Medium severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2019-12904 libgcrypt 1.8.3-r0 1.8.3-r1

Low severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2018-20679 busybox 1.28.4-r3 1.30.0

Negligible severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2019-5747 busybox 1.28.4-r3 none

Resourse policy name:

Resourse policy application scopes: ["group"]

================================================ FILE: regoservice/testdata/goldens/trivy-jira.golden ================================================ h1. Image name: pom.xml h4. Critical severity vulnerabilities | || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url | | 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 | | 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 h4. High severity vulnerabilities | || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url | | 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 | | 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 h4. Medium severity vulnerabilities | || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url | | 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 h4. Low severity vulnerabilities | || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url | | 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 h4. Unknown severity vulnerabilities | || Layer || Title || Vulnerability ID || Resource name || Path || Installed version || Fix version || Url | | 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 ================================================ FILE: regoservice/testdata/goldens/trivy-operator-jira.golden ================================================ h1. Image: library/nginx:1.16 in namespace default h4. Summary totals: |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}| h4. Critical severity vulnerabilities || ID || Title || Resource || Installed version || Fixed version || Url || | CVE-2019-20367 | | libbsd0 | 0.9.1-2 | 0.9.1-2+deb10u1 | https://avd.aquasec.com/nvd/cve-2019-20367 | h4. High severity vulnerabilities || ID || Title || Resource || Installed version || Fixed version || Url || | CVE-2018-25009 | libwebp: out-of-bounds read in WebPMuxCreateInternal | libwebp6 | 0.6.1-2 | | https://avd.aquasec.com/nvd/cve-2018-25009 | h4. Medium severity vulnerabilities || ID || Title || Resource || Installed version || Fixed version || Url || | CVE-2018-25010 | libwebp: out-of-bounds read in WebPMuxCreateInternal | libwebp3 | 0.6.1-2 | | https://avd.aquasec.com/nvd/cve-2018-25009 | h4. Low severity vulnerabilities || ID || Title || Resource || Installed version || Fixed version || Url || | CVE-2018-25011 | libwebp: out-of-bounds read in WebPMuxCreateInternal | libwebp4 | 0.6.1-2 | | https://avd.aquasec.com/nvd/cve-2018-25009 | h4. Unknown severity vulnerabilities || ID || Title || Resource || Installed version || Fixed version || Url || | CVE-2018-25012 | libwebp: out-of-bounds read in WebPMuxCreateInternal | libwebp5 | 0.6.1-2 | | https://avd.aquasec.com/nvd/cve-2018-25009 | ================================================ FILE: regoservice/testdata/goldens/trivy-operator-slack.golden ================================================ [{"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"}] ================================================ FILE: regoservice/testdata/goldens/trivy-vulns-slack.golden ================================================ [{"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"}] ================================================ FILE: regoservice/testdata/goldens/vuls-cyclonedx.golden ================================================ libgcrypt 1.8.3-r0 LGPL CVE-2019-12904 https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-12904 4.3 4.3 4.3 medium CVSS V2 AV:N/AC:M/Au:N/C:P/I:N/A:N Upgrade package libgcrypt to version 1.8.3-r1 or above. CVE-2021-33560 https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-33560 5 5 5 high CVSS V2 AV:N/AC:L/Au:N/C:P/I:N/A:N No solution available busybox 1.28.4-r3 GPL2 CVE-2018-1000517 https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1000517 7.5 7.5 7.5 critical CVSS V2 AV:N/AC:L/Au:N/C:P/I:P/A:P No solution available CVE-2018-20679 https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-20679 5 5 5 low CVSS V2 AV:N/AC:L/Au:N/C:P/I:N/A:N No solution available CVE-2019-5747 https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-5747 5 5 5 negligible CVSS V2 AV:N/AC:L/Au:N/C:P/I:N/A:N No solution available ================================================ FILE: regoservice/testdata/goldens/vuls-html.golden ================================================

Image name: all-in-one:3.5.19223

Registry: Aqua

Image is compliant

Malware found: Yes

Sensitive data found: Yes

critical 1
high 1
medium 1
low 1
negligible 1

Assurance controls

# Control Policy Name Status
1 malware Default PASS
2 license Default PASS
3 max_severity Default PASS

Critical severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2018-1000517 busybox 1.28.4-r3 1.29.0

High severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2021-33560 libgcrypt 1.8.3-r0 1.8.8

Medium severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2019-12904 libgcrypt 1.8.3-r0 1.8.3-r1

Low severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2018-20679 busybox 1.28.4-r3 1.30.0

Negligible severity vulnerabilities

Vulnerability ID Resource name Installed version Fix version
CVE-2019-5747 busybox 1.28.4-r3 none
================================================ FILE: regoservice/testdata/goldens/vuls-slack.golden ================================================ [{"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"}] ================================================ FILE: regoservice/testdata/inputs/aqua-incident-input.json ================================================ { "create_time": 0, "event_category": "incident", "type": "Malware", "host_name": "ubuntu", "host_id": "test", "category": "Test", "result": 2, "severity_score": 3, "name": "test", "response_policy_name": "test", "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\"}", "application_scope": [ "scope1", "scope2" ] } ================================================ FILE: regoservice/testdata/inputs/aqua-input.json ================================================ { "image": "all-in-one:3.5.19223", "registry": "Aqua", "scan_started": { "seconds": 1624544066, "nanos": 881635578 }, "scan_duration": 3, "pull_skipped": true, "image_size": 178041649, "digest": "sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf", "os": "alpine", "version": "3.8.4", "resources": [ { "resource": { "format": "apk", "name": "libgcrypt", "version": "1.8.3-r0", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:libgcrypt:1.8.3-r0", "license": "LGPL", "layer_digest": "sha256:18282a568fb5f423d55ad20369f729a22e3a912da9ddd0bff0b063c81544f785", "src_name": "libgcrypt", "src_version": "1.8.3-r0" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2019-12904", "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.", "nvd_score": 4.3, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "nvd_severity": "medium", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-12904", "vendor_score_version": "CVSS v2", "publish_date": "2019-06-20", "modification_date": "2021-03-04", "fix_version": "1.8.3-r1", "solution": "Upgrade package libgcrypt to version 1.8.3-r1 or above.", "nvd_score_v3": 5.9, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "medium", "aqua_score": 4.3, "aqua_severity": "medium", "aqua_vectors": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "heuristic_ref_id": 328557, "aqua_severity_classification": "NVD CVSS V2 Score: 4.3", "aqua_score_classification": "NVD CVSS V2 Score: 4.3" }, { "name": "CVE-2021-33560", "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.", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "high", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-33560", "publish_date": "2021-06-08", "modification_date": "2021-06-15", "fix_version": "1.8.8", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "high", "aqua_score": 5, "aqua_severity": "high", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" } ] }, { "resource": { "format": "apk", "name": "busybox", "version": "1.28.4-r3", "arch": "x86_64", "cpe": "pkg:/alpine:3.8.4:busybox:1.28.4-r3", "license": "GPL2", "layer_digest": "sha256:c87736221ed0bcaa60b8e92a19bec2284899ef89226f2a07968677cf59e637a4", "src_name": "busybox", "src_version": "1.28.4-r3" }, "scanned": true, "vulnerabilities": [ { "name": "CVE-2018-1000517", "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.", "nvd_score": 7.5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "nvd_severity": "critical", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-1000517", "publish_date": "2018-06-26", "modification_date": "2021-02-18", "fix_version": "1.29.0", "nvd_score_v3": 9.8, "nvd_vectors_v3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "nvd_severity_v3": "critical", "aqua_score": 7.5, "aqua_severity": "critical", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 7.5", "aqua_score_classification": "NVD CVSS V2 Score: 7.5" }, { "name": "CVE-2018-20679", "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.", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "low", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-20679", "publish_date": "2019-01-09", "modification_date": "2019-09-04", "fix_version": "1.30.0", "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "low", "aqua_score": 5, "aqua_severity": "low", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" }, { "name": "CVE-2019-5747", "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.", "nvd_score": 5, "nvd_score_version": "CVSS v2", "nvd_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "nvd_severity": "negligible", "nvd_url": "https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-5747", "publish_date": "2019-01-09", "modification_date": "2019-09-04", "already_acknowledged": true, "nvd_score_v3": 7.5, "nvd_vectors_v3": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", "nvd_severity_v3": "negligible", "aqua_score": 5, "aqua_severity": "negligible", "aqua_vectors": "AV:N/AC:L/Au:N/C:P/I:N/A:N", "aqua_scoring_system": "CVSS V2", "aqua_severity_classification": "NVD CVSS V2 Score: 5.0", "aqua_score_classification": "NVD CVSS V2 Score: 5.0" } ] } ], "image_assurance_results": { "checks_performed": [ { "policy_id": 1, "policy_name": "Default", "control": "malware" }, { "policy_id": 1, "policy_name": "Default", "control": "license" }, { "policy_id": 1, "policy_name": "Default", "control": "max_severity", "maximum_severity_allowed": "critical", "maximum_severity_found": "high" } ] }, "vulnerability_summary": { "total": 5, "critical": 1, "high": 1, "medium": 1, "low": 1, "negligible": 1, "sensitive": 0, "malware": 0, "score_average": 5.020931 }, "scan_options": { "scan_executables": true, "scan_sensitive_data": true, "show_will_not_fix": true, "webhook_url": "https://975cb1e5b1fc.ngrok.io", "scan_malware": true, "strict_scan": true, "scan_files": true, "scan_timeout": 3600000000000, "manual_pull_fallback": true, "dockerless": true, "enable_fast_scanning": true, "memoryThrottling": true, "suggest_os_upgrade": true, "seim_enabled": true }, "previous_digest": "sha256:45388de11cfbf5c5d9e2e1418dfeac221c57cfffa1e2fffa833ac283ed029ecf", "vulnerability_diff": { "total": 0, "critical": 0, "high": 0, "medium": 0, "low": 0, "negligible": 0, "sensitive": 0, "malware": 0 }, "initiating_user": "upwork", "data_date": 1624490283, "pull_name": "registry.aquasec.com/all-in-one:3.5.19223", "changed_result": false, "function_metadata": {}, "scan_id": 386815, "required_image_platform": "amd64:::", "scanned_image_platform": "amd64::linux:", "security_feeds_used": { "executables": "92475757e80429" }, "image_id": 704, "internal_digest_id": { "id": 1095 }, "application_scope": [ "group" ], "application_scope_owners": [ "owner" ] } ================================================ FILE: regoservice/testdata/inputs/aqua-insight-input.json ================================================ { "account_id": "4700", "customTriggerType": "custom-insight", "evidence": { "malware": null, "malware_remediation": "", "privileged_iam_roles": null, "privileged_iam_roles_remediation": null, "privileged_workloads": null, "privileged_workloads_remediation": null, "sensitive_data": [ { "file_name": "", "file_path": "/private-key.pem", "file_type": "RSA PRIVATE KEY", "function": "", "image": "shayyo/sensitive_data:latest" } ], "sensitive_data_remediation": "", "vulnerabilities": null, "vulnerabilities_remediation": null }, "global_id": "bab1e6eb-5017-42f3-b419-5db95464e443", "id": "6131180", "insight": { "category": "", "description": "Workloads or images containing login data", "id": "aqua-3006", "impact": "Attackers with access to this workload or image might be able to use the login data to gain initial access to other resources", "instance": 0, "priority": 2, "rank": 380, "suppress_resources": 0, "suppress_resources_with_expiration": 0, "total_impacted_resources": 0 }, "postee": { "AquaServer": "" }, "resource": { "arn": "", "cluster_arn": "", "comment": "", "found_date": "2022-08-25T09:02:28.991Z", "full_path": "", "global_id": "3614f379-0ac0-4074-8bb7-8bc7b1fd5b26", "id": "6131180", "internet_exposure_status": "unknown", "is_connected": false, "is_suppressed": false, "last_scanned": "2022-08-25T08:59:42.314673Z", "name": "shayyo/sensitive_data:latest", "num_pods": 0, "short_path": "", "steps": { "Image": "sensitive_data:latest", "Registry": "Docker Hub", "RegistryDomain": "docker.io" }, "suppress": { "configuration": { "description": "", "duration_days": 0, "user": "", "with_expiration": false }, "date": "0001-01-01T00:00:00Z", "expiration_date": "0001-01-01T00:00:00Z" } }, "resourceTypeKey": "", "application_scope": ["scope1","scope2"], "response_policy_id": "11", "response_policy_name": "insights_login data", "type": "Image"} ================================================ FILE: regoservice/testdata/inputs/simple-input.json ================================================ { "user": "demo" } ================================================ FILE: regoservice/testdata/inputs/trivy-input.json ================================================ { "SchemaVersion": 2, "ArtifactName": "pom.xml", "ArtifactType": "filesystem", "Metadata": { "ImageConfig": { "architecture": "", "created": "0001-01-01T00:00:00Z", "os": "", "rootfs": { "type": "", "diff_ids": null }, "config": {} } }, "Results": [ { "Target": "pom.xml", "Class": "lang-pkgs", "Type": "pom", "Vulnerabilities": [ { "VulnerabilityID": "CVE-2019-17571", "PkgName": "log4j:log4j", "InstalledVersion": "1.2.17", "FixedVersion": "2.0-alpha1", "Status": "fixed", "Layer": {}, "SeveritySource": "nvd", "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-17571", "DataSource": { "ID": "glad", "Name": "GitLab Advisory Database Community", "URL": "https://gitlab.com/gitlab-org/advisories-community" }, "Title": "log4j: deserialization of untrusted data in SocketServer", "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.", "Severity": "CRITICAL", "CweIDs": [ "CWE-502" ], "CVSS": { "ghsa": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "V3Score": 9.8 }, "nvd": { "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "V2Score": 7.5, "V3Score": 9.8 }, "redhat": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "V3Score": 9.8 } }, "References": [ "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00022.html", "https://access.redhat.com/security/cve/CVE-2019-17571", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-17571", "https://github.com/advisories/GHSA-2qrg-x229-3v8q", "https://lists.apache.org/thread.html/277b4b5c2b0e06a825ccec565fa65bd671f35a4d58e3e2ec5d0618e1@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/44491fb9cc19acc901f7cff34acb7376619f15638439416e3e14761c@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/479471e6debd608c837b9815b76eab24676657d4444fcfd5ef96d6e6@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/564f03b4e9511fcba29c68fc0299372dadbdb002718fa8edcc4325e4@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/6114ce566200d76e3cc45c521a62c2c5a4eac15738248f58a99f622c@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/752ec92cd1e334a639e79bfbd689a4ec2c6579ec5bb41b53ffdf358d@%3Cdev.kafka.apache.org%3E", "https://lists.apache.org/thread.html/8ab32b4c9f1826f20add7c40be08909de9f58a89dc1de9c09953f5ac@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/eea03d504b36e8f870e8321d908e1def1addda16adda04327fe7c125%40%3Cdev.logging.apache.org%3E", "https://lists.apache.org/thread.html/r05755112a8c164abc1004bb44f198b1e3d8ca3d546a8f13ebd3aa05f@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r107c8737db39ec9ec4f4e7147b249e29be79170b9ef4b80528105a2d@%3Cdev.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r13d4b5c60ff63f3c4fab51d6ff266655be503b8a1884e2f2fab67c3a@%3Ccommon-issues.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/r189aaeaad897f7d6b96f7c43a8ef2dfb9f6e9f8c1cc9ad182ce9b9ae@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r18f1c010b554a3a2d761e8ffffd8674fd4747bcbcf16c643d708318c@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r1b103833cb5bc8466e24ff0ecc5e75b45a705334ab6a444e64e840a0@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/r1b7734dfdfd938640f2f5fb6f4231a267145c71ed60cc7faa1cbac07@%3Ccommon-issues.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/r26244f9f7d9a8a27a092eb0b2a0ca9395e88fcde8b5edaeca7ce569c@%3Ccommon-issues.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/r2721aba31a8562639c4b937150897e24f78f747cdbda8641c0f659fe@%3Cusers.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r2756fd570b6709d55a61831ca028405bcb3e312175a60bc5d911c81f@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r2ce8d26154bea939536e6cf27ed02d3192bf5c5d04df885a80fe89b3@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r2ff63f210842a3c5e42f03a35d8f3a345134d073c80a04077341c211@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r3543ead2317dcd3306f69ee37b07dd383dbba6e2f47ff11eb55879ad@%3Cusers.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r356d57d6225f91fdc30f8b0a2bed229d1ece55e16e552878c5fa809a@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r3784834e80df2f284577a5596340fb84346c91a2dea6a073e65e3397@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r3a85514a518f3080ab1fc2652cfe122c2ccf67cfb32356acb1b08fe8@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/r3bf7b982dfa0779f8a71f843d2aa6b4184a53e6be7f149ee079387fd@%3Cdev.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r3c575cabc7386e646fb12cb82b0b38ae5a6ade8a800f827107824495@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r3cf50d05ce8cec8c09392624b7bae750e7643dae60ef2438641ee015@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r3d666e4e8905157f3c046d31398b04f2bfd4519e31f266de108c6919@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r48d5019bd42e0770f7e5351e420a63a41ff1f16924942442c6aff6a8@%3Ccommits.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r48efc7cb5aeb4e1f67aaa06fb4b5479a5635d12f07d0b93fc2d08809@%3Ccommits.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r4ac89cbecd9e298ae9fafb5afda6fa77ac75c78d1ac957837e066c4e@%3Cuser.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r4b25538be50126194cc646836c718b1a4d8f71bd9c912af5b59134ad@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/r52a5129df402352adc34d052bab9234c8ef63596306506a89fdc7328@%3Cusers.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r594411f4bddebaf48a4c70266d0b7849e0d82bb72826f61b3a35bba7@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/r5c084578b3e3b40bd903c9d9e525097421bcd88178e672f612102eb2@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r61590890edcc64140e0c606954b29a063c3d08a2b41d447256d51a78@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r61db8e7dcb56dc000a5387a88f7a473bacec5ee01b9ff3f55308aacc@%3Cdev.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r61db8e7dcb56dc000a5387a88f7a473bacec5ee01b9ff3f55308aacc@%3Cusers.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r6236b5f8646d48af8b66d5050f288304016840788e508c883356fe0e@%3Clog4j-user.logging.apache.org%3E", "https://lists.apache.org/thread.html/r681b4432d0605f327b68b9f8a42662993e699d04614de4851c35ffd1@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/r696507338dd5f44efc23d98cafe30f217cf3ba78e77ed1324c7a5179@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r6aec6b8f70167fa325fb98b3b5c9ce0ffaed026e697b69b85ac24628@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r6b45a2fcc8e98ac93a179183dbb7f340027bdb8e3ab393418076b153@%3Ccommon-issues.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/r6d34da5a0ca17ab08179a30c971446c7421af0e96f6d60867eabfc52@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/r71e26f9c2d5826c6f95ad60f7d052d75e1e70b0d2dd853db6fc26d5f@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r746fbc3fc13aee292ae6851f7a5080f592fa3a67b983c6887cdb1fc5@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/r7a1acc95373105169bd44df710c2f462cad31fb805364d2958a5ee03@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r7bcdc710857725c311b856c0b82cee6207178af5dcde1bd43d289826@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r7f462c69d5ded4c0223e014d95a3496690423c5f6f05c09e2f2a407a@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r8244fd0831db894d5e89911ded9c72196d395a90ae655414d23ed0dd@%3Cusers.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r8418a0dff1729f19cf1024937e23a2db4c0f94f2794a423f5c10e8e7@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/r8890b8f18f1de821595792b58b968a89692a255bc20d86d395270740@%3Ccommits.druid.apache.org%3E", "https://lists.apache.org/thread.html/r8a1cfd4705258c106e488091fcec85f194c82f2bbde6bd151e201870@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r8c392ca48bb7e50754e4bc05865e9731b23d568d18a520fe3d8c1f75@%3Ccommon-issues.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/r8c6300245c0bcef095e9f07b48157e2c6471df0816db3408fcf1d748@%3Ccommon-issues.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/r8d78a0fbb56d505461e29868d1026e98c402e6a568c13a6da67896a2@%3Cdev.jena.apache.org%3E", "https://lists.apache.org/thread.html/r8e3f7da12bf5750b0a02e69a78a61073a2ac950eed7451ce70a65177@%3Ccommits.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r909b8e3a36913944d3b7bafe9635d4ca84f8f0e2cd146a1784f667c2@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r90c23eb8c82835fa82df85ae5e88c81fd9241e20a22971b0fb8f2c34@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/r944183c871594fe9a555b8519a7c945bbcf6714d72461aa6c929028f@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r9a9e3b42cd5d1c4536a14ef04f75048dec8e2740ac6a138ea912177f@%3Cpluto-dev.portals.apache.org%3E", "https://lists.apache.org/thread.html/r9d0d03f2e7d9e13c68b530f81d02b0fec33133edcf27330d8089fcfb@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r9d2e28e71f91ba0b6f4114c8ecd96e2b1f7e0d06bdf8eb768c183aa9@%3Ccommon-issues.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/r9dc2505651788ac668299774d9e7af4dc616be2f56fdc684d1170882@%3Cusers.activemq.apache.org%3E", "https://lists.apache.org/thread.html/r9fb3238cfc3222f2392ca6517353aadae18f76866157318ac562e706@%3Ccommon-issues.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/ra18a903f785aed9403aea38bc6f36844a056283c00dcfc6936b6318c@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/ra38785cfc0e7f17f8e24bebf775dd032c033fadcaea29e5bc9fffc60@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/ra54fa49be3e773d99ccc9c2a422311cf77e3ecd3b8594ee93043a6b1@%3Cdev.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/ra9611a8431cb62369bce8909d7645597e1dd45c24b448836b1e54940@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/raedd12dc24412b3780432bf202a2618a21a727788543e5337a458ead@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/rb1b29aee737e1c37fe1d48528cb0febac4f5deed51f5412e6fdfe2bf@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/rb3c94619728c8f8c176d8e175e0a1086ca737ecdfcd5a2214bb768bc@%3Ccommits.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/rbc45eb0f53fd6242af3e666c2189464f848a851d408289840cecc6e3@%3Ccommits.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rbd19de368abf0764e4383ec44d527bc9870176f488a494f09a40500d@%3Ccommon-dev.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/rbdf18e39428b5c80fc35113470198b1fe53b287a76a46b0f8780b5fd@%3Cdev.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rbf4ce74b0d1fa9810dec50ba3ace0caeea677af7c27a97111c06ccb7@%3Cdev.kafka.apache.org%3E", "https://lists.apache.org/thread.html/rbf4ce74b0d1fa9810dec50ba3ace0caeea677af7c27a97111c06ccb7@%3Cusers.kafka.apache.org%3E", "https://lists.apache.org/thread.html/rc17d8491beee51607693019857e41e769795366b85be00aa2f4b3159@%3Cnotifications.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rc1eaed7f7d774d5d02f66e49baced31e04827a1293d61a70bd003ca7@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/rc628307962ae1b8cc2d21b8e4b7dd6d7755b2dd52fa56a151a27e4fd@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rca24a281000fb681d7e26e5c031a21eb4b0593a7735f781b53dae4e2@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/rcd71280585425dad7e232f239c5709e425efdd0d3de4a92f808a4767@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/rd0e44e8ef71eeaaa3cf3d1b8b41eb25894372e2995ec908ce7624d26@%3Ccommits.pulsar.apache.org%3E", "https://lists.apache.org/thread.html/rd3a9511eebab60e23f224841390a3f8cd5358cff605c5f7042171e47@%3Cdev.tinkerpop.apache.org%3E", "https://lists.apache.org/thread.html/rd5dbeee4808c0f2b9b51479b50de3cc6adb1072c332a200d9107f13e@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/rd6254837403e8cbfc7018baa9be29705f3f06bd007c83708f9a97679@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rd7805c1bf9388968508c6c8f84588773216e560055ddcc813d19f347@%3Ccommon-issues.hadoop.apache.org%3E", "https://lists.apache.org/thread.html/rd882ab6b642fe59cbbe94dc02bd197342058208f482e57b537940a4b@%3Cpluto-dev.portals.apache.org%3E", "https://lists.apache.org/thread.html/rda4849c6823dd3e83c7a356eb883180811d5c28359fe46865fd151c3@%3Cusers.kafka.apache.org%3E", "https://lists.apache.org/thread.html/rdb7ddf28807e27c7801f6e56a0dfb31092d34c61bdd4fa2de9182119@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/rdec0d8ac1f03e6905b0de2df1d5fcdb98b94556e4f6cccf7519fdb26@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/rdf2a0d94c3b5b523aeff7741ae71347415276062811b687f30ea6573@%3Ccommits.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/re36da78e4f3955ba6c1c373a2ab85a4deb215ca74b85fcd66142fea1@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/re8c21ed9dd218c217d242ffa90778428e446b082b5e1c29f567e8374@%3Cissues.activemq.apache.org%3E", "https://lists.apache.org/thread.html/reaf6b996f74f12b4557bc221abe88f58270ac583942fa41293c61f94@%3Cpluto-scm.portals.apache.org%3E", "https://lists.apache.org/thread.html/rec34b1cccf907898e7cb36051ffac3ccf1ea89d0b261a2a3b3fb267f@%3Ccommits.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rf1b434e11834a4449cd7addb69ed0aef0923112b5938182b363a968c@%3Cnotifications.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rf2567488cfc9212b42e34c6393cfa1c14e30e4838b98dda84d71041f@%3Cdev.tika.apache.org%3E", "https://lists.apache.org/thread.html/rf53eeefb7e7e524deaacb9f8671cbf01b8a253e865fb94e7656722c0@%3Cissues.bookkeeper.apache.org%3E", "https://lists.apache.org/thread.html/rf77f79699c8d7e430c14cf480f12ed1297e6e8cf2ed379a425941e80@%3Cpluto-dev.portals.apache.org%3E", "https://lists.apache.org/thread.html/rf9c19bcc2f7a98a880fa3e3456c003d331812b55836b34ef648063c9@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", "https://lists.apache.org/thread.html/rfdf65fa675c64a64459817344e0e6c44d51ee264beea6e5851fb60dc@%3Cissues.bookkeeper.apache.org%3E", "https://lists.debian.org/debian-lts-announce/2020/01/msg00008.html", "https://nvd.nist.gov/vuln/detail/CVE-2019-17571", "https://security.netapp.com/advisory/ntap-20200110-0001/", "https://ubuntu.com/security/notices/USN-4495-1", "https://ubuntu.com/security/notices/USN-5998-1", "https://usn.ubuntu.com/4495-1/", "https://www.cve.org/CVERecord?id=CVE-2019-17571", "https://www.debian.org/security/2020/dsa-4686", "https://www.oracle.com/security-alerts/cpuApr2021.html", "https://www.oracle.com/security-alerts/cpuapr2020.html", "https://www.oracle.com/security-alerts/cpuapr2022.html", "https://www.oracle.com/security-alerts/cpujul2020.html", "https://www.oracle.com/security-alerts/cpujul2022.html" ], "PublishedDate": "2019-12-20T17:15:00Z", "LastModifiedDate": "2022-12-14T17:50:00Z" }, { "VulnerabilityID": "CVE-2022-23305", "PkgName": "log4j:log4j", "InstalledVersion": "1.2.17", "Status": "affected", "Layer": {}, "SeveritySource": "nvd", "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-23305", "DataSource": { "ID": "glad", "Name": "GitLab Advisory Database Community", "URL": "https://gitlab.com/gitlab-org/advisories-community" }, "Title": "log4j: SQL injection in Log4j 1.x when application is configured to use JDBCAppender", "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.", "Severity": "CRITICAL", "CweIDs": [ "CWE-89" ], "CVSS": { "ghsa": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "V3Score": 9.8 }, "nvd": { "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:P", "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "V2Score": 6.8, "V3Score": 9.8 }, "redhat": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "V3Score": 8.8 } }, "References": [ "http://www.openwall.com/lists/oss-security/2022/01/18/4", "https://access.redhat.com/security/cve/CVE-2022-23305", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-23305", "https://errata.almalinux.org/8/ALSA-2022-0290.html", "https://github.com/advisories/GHSA-65fg-84f6-3jq3", "https://linux.oracle.com/cve/CVE-2022-23305.html", "https://linux.oracle.com/errata/ELSA-2022-9419.html", "https://lists.apache.org/thread/pt6lh3pbsvxqlwlp4c5l798dv2hkc85y", "https://logging.apache.org/log4j/1.2/index.html", "https://nvd.nist.gov/vuln/detail/CVE-2022-23305", "https://security.netapp.com/advisory/ntap-20220217-0007/", "https://ubuntu.com/security/notices/USN-5998-1", "https://www.cve.org/CVERecord?id=CVE-2022-23305", "https://www.openwall.com/lists/oss-security/2022/01/18/4", "https://www.oracle.com/security-alerts/cpuapr2022.html", "https://www.oracle.com/security-alerts/cpujul2022.html" ], "PublishedDate": "2022-01-18T16:15:00Z", "LastModifiedDate": "2023-02-24T15:30:00Z" }, { "VulnerabilityID": "CVE-2021-4104", "PkgName": "log4j:log4j", "InstalledVersion": "1.2.17", "Status": "affected", "Layer": {}, "SeveritySource": "nvd", "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-4104", "DataSource": { "ID": "glad", "Name": "GitLab Advisory Database Community", "URL": "https://gitlab.com/gitlab-org/advisories-community" }, "Title": "Remote code execution in Log4j 1.x when application is configured to use JMSAppender", "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.", "Severity": "HIGH", "CweIDs": [ "CWE-502" ], "CVSS": { "ghsa": { "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", "V3Score": 8.1 }, "nvd": { "V2Vector": "AV:N/AC:M/Au:S/C:P/I:P/A:P", "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H", "V2Score": 6, "V3Score": 7.5 }, "redhat": { "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H", "V3Score": 7.5 } }, "References": [ "http://www.openwall.com/lists/oss-security/2022/01/18/3", "https://access.redhat.com/security/cve/CVE-2021-4104", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-4104", "https://errata.almalinux.org/8/ALSA-2022-0290.html", "https://github.com/advisories/GHSA-fp5r-v3w9-4333", "https://github.com/apache/logging-log4j2/pull/608#issuecomment-990494126", "https://github.com/apache/logging-log4j2/pull/608#issuecomment-991723301", "https://linux.oracle.com/cve/CVE-2021-4104.html", "https://linux.oracle.com/errata/ELSA-2022-9056.html", "https://lists.apache.org/thread/0x4zvtq92yggdgvwfgsftqrj4xx5w0nx", "https://nvd.nist.gov/vuln/detail/CVE-2021-4104", "https://psirt.global.sonicwall.com/vuln-detail/SNWLID-2021-0033", "https://security.gentoo.org/glsa/202209-02", "https://security.netapp.com/advisory/ntap-20211223-0007/", "https://ubuntu.com/security/notices/USN-5223-1", "https://ubuntu.com/security/notices/USN-5223-2", "https://www.cve.org/CVERecord?id=CVE-2021-4104", "https://www.cve.org/CVERecord?id=CVE-2021-44228", "https://www.kb.cert.org/vuls/id/930724", "https://www.openwall.com/lists/oss-security/2021/12/13/1", "https://www.openwall.com/lists/oss-security/2021/12/13/2", "https://www.oracle.com/security-alerts/cpuapr2022.html", "https://www.oracle.com/security-alerts/cpujan2022.html", "https://www.oracle.com/security-alerts/cpujul2022.html" ], "PublishedDate": "2021-12-14T12:15:00Z", "LastModifiedDate": "2022-10-05T17:53:00Z" }, { "VulnerabilityID": "CVE-2022-23302", "PkgName": "log4j:log4j", "InstalledVersion": "1.2.17", "Status": "affected", "Layer": {}, "SeveritySource": "nvd", "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-23302", "DataSource": { "ID": "glad", "Name": "GitLab Advisory Database Community", "URL": "https://gitlab.com/gitlab-org/advisories-community" }, "Title": "log4j: Remote code execution in Log4j 1.x when application is configured to use JMSSink", "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.", "Severity": "MEDIUM", "CweIDs": [ "CWE-502" ], "CVSS": { "ghsa": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "V3Score": 8.8 }, "nvd": { "V2Vector": "AV:N/AC:M/Au:S/C:P/I:P/A:P", "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "V2Score": 6, "V3Score": 8.8 }, "redhat": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "V3Score": 8.8 } }, "References": [ "http://www.openwall.com/lists/oss-security/2022/01/18/3", "https://access.redhat.com/security/cve/CVE-2022-23302", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-23302", "https://errata.almalinux.org/8/ALSA-2022-0290.html", "https://github.com/advisories/GHSA-w9p3-5cr8-m3jj", "https://linux.oracle.com/cve/CVE-2022-23302.html", "https://linux.oracle.com/errata/ELSA-2022-9419.html", "https://lists.apache.org/thread/bsr3l5qz4g0myrjhy9h67bcxodpkwj4w", "https://logging.apache.org/log4j/1.2/index.html", "https://nvd.nist.gov/vuln/detail/CVE-2022-23302", "https://security.netapp.com/advisory/ntap-20220217-0006/", "https://ubuntu.com/security/notices/USN-5998-1", "https://www.cve.org/CVERecord?id=CVE-2022-23302", "https://www.openwall.com/lists/oss-security/2022/01/18/3", "https://www.oracle.com/security-alerts/cpuapr2022.html", "https://www.oracle.com/security-alerts/cpujul2022.html" ], "PublishedDate": "2022-01-18T16:15:00Z", "LastModifiedDate": "2023-02-24T15:30:00Z" }, { "VulnerabilityID": "CVE-2022-23307", "PkgName": "log4j:log4j", "InstalledVersion": "1.2.17", "Status": "affected", "Layer": {}, "SeveritySource": "nvd", "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-23307", "DataSource": { "ID": "glad", "Name": "GitLab Advisory Database Community", "URL": "https://gitlab.com/gitlab-org/advisories-community" }, "Title": "log4j: Unsafe deserialization flaw in Chainsaw log viewer", "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.", "Severity": "HIGH", "CweIDs": [ "CWE-502" ], "CVSS": { "ghsa": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", "V3Score": 9.8 }, "nvd": { "V2Vector": "AV:N/AC:L/Au:S/C:C/I:C/A:C", "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "V2Score": 9, "V3Score": 8.8 }, "redhat": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", "V3Score": 8.8 } }, "References": [ "https://access.redhat.com/security/cve/CVE-2022-23307", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-23307", "https://errata.almalinux.org/8/ALSA-2022-0290.html", "https://github.com/advisories/GHSA-f7vh-qwp3-x37m", "https://linux.oracle.com/cve/CVE-2022-23307.html", "https://linux.oracle.com/errata/ELSA-2022-9419.html", "https://lists.apache.org/thread/rg4yyc89vs3dw6kpy3r92xop9loywyhh", "https://logging.apache.org/log4j/1.2/index.html", "https://nvd.nist.gov/vuln/detail/CVE-2022-23307", "https://ubuntu.com/security/notices/USN-5998-1", "https://www.cve.org/CVERecord?id=CVE-2022-23307", "https://www.openwall.com/lists/oss-security/2022/01/18/5", "https://www.oracle.com/security-alerts/cpuapr2022.html", "https://www.oracle.com/security-alerts/cpujul2022.html" ], "PublishedDate": "2022-01-18T16:15:00Z", "LastModifiedDate": "2023-02-24T15:29:00Z" }, { "VulnerabilityID": "CVE-2023-26464", "PkgName": "log4j:log4j", "InstalledVersion": "1.2.17", "FixedVersion": "2.0", "Status": "fixed", "Layer": {}, "SeveritySource": "nvd", "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-26464", "DataSource": { "ID": "glad", "Name": "GitLab Advisory Database Community", "URL": "https://gitlab.com/gitlab-org/advisories-community" }, "Title": "DoS via hashmap logging", "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", "Severity": "UNKNOWN", "CweIDs": [ "CWE-502" ], "CVSS": { "ghsa": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "V3Score": 7.5 }, "nvd": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "V3Score": 7.5 }, "redhat": { "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", "V3Score": 7.5 } }, "References": [ "https://access.redhat.com/security/cve/CVE-2023-26464", "https://github.com/advisories/GHSA-vp98-w2p3-mv35", "https://lists.apache.org/thread/wkx6grrcjkh86crr49p4blc1v1nflj3t", "https://nvd.nist.gov/vuln/detail/CVE-2023-26464", "https://security.netapp.com/advisory/ntap-20230505-0008/", "https://www.cve.org/CVERecord?id=CVE-2023-26464" ], "PublishedDate": "2023-03-10T14:15:00Z", "LastModifiedDate": "2023-05-05T20:15:00Z" }, { "VulnerabilityID": "CVE-2020-9488", "PkgName": "log4j:log4j", "InstalledVersion": "1.2.17", "FixedVersion": "2.12.3, 2.13.2", "Status": "fixed", "Layer": {}, "SeveritySource": "nvd", "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-9488", "DataSource": { "ID": "glad", "Name": "GitLab Advisory Database Community", "URL": "https://gitlab.com/gitlab-org/advisories-community" }, "Title": "log4j: improper validation of certificate with host mismatch in SMTP appender", "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", "Severity": "LOW", "CweIDs": [ "CWE-295" ], "CVSS": { "ghsa": { "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", "V3Score": 3.7 }, "nvd": { "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", "V2Score": 4.3, "V3Score": 3.7 }, "redhat": { "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", "V3Score": 3.7 } }, "References": [ "https://access.redhat.com/security/cve/CVE-2020-9488", "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-9488", "https://gitbox.apache.org/repos/asf?p=logging-log4j2.git;h=6851b5083ef9610bae320bf07e1f24d2aa08851b (release-2.x)", "https://gitbox.apache.org/repos/asf?p=logging-log4j2.git;h=fb91a3d71e2f3dadad6fd1beb2ab857f44fe8bbb (master)", "https://github.com/advisories/GHSA-vwqq-5vrc-xw9h", "https://issues.apache.org/jira/browse/LOG4J2-2819", "https://lists.apache.org/thread.html/r0a2699f724156a558afd1abb6c044fb9132caa66dce861b82699722a@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r0df3d7a5acb98c57e64ab9266aa21eeee1d9b399addb96f9cf1cbe05@%3Cdev.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r1fc73f0e16ec2fa249d3ad39a5194afb9cc5afb4c023dc0bab5a5881@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/r22a56beb76dd8cf18e24fda9072f1e05990f49d6439662d3782a392f@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/r2721aba31a8562639c4b937150897e24f78f747cdbda8641c0f659fe@%3Cusers.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r2f209d271349bafd91537a558a279c08ebcff8fa3e547357d58833e6@%3Cdev.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r33864a0fc171c1c4bf680645ebb6d4f8057899ab294a43e1e4fe9d04@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/r393943de452406f0f6f4b3def9f8d3c071f96323c1f6ed1a098f7fe4@%3Ctorque-dev.db.apache.org%3E", "https://lists.apache.org/thread.html/r3d1d00441c55144a4013adda74b051ae7864128ebcfb6ee9721a2eb3@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/r4285398e5585a0456d3d9db021a4fce6e6fcf3ec027dfa13a450ec98@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r45916179811a32cbaa500f972de9098e6ee80ee81c7f134fce83e03a@%3Cissues.flink.apache.org%3E", "https://lists.apache.org/thread.html/r48bcd06049c1779ef709564544c3d8a32ae6ee5c3b7281a606ac4463@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r48efc7cb5aeb4e1f67aaa06fb4b5479a5635d12f07d0b93fc2d08809@%3Ccommits.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r4d5dc9f3520071338d9ebc26f9f158a43ae28a91923d176b550a807b@%3Cdev.hive.apache.org%3E", "https://lists.apache.org/thread.html/r4db540cafc5d7232c62e076051ef661d37d345015b2e59b3f81a932f@%3Cdev.hive.apache.org%3E", "https://lists.apache.org/thread.html/r4ed1f49616a8603832d378cb9d13e7a8b9b27972bb46d946ccd8491f@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/r5a68258e5ab12532dc179edae3d6e87037fa3b50ab9d63a90c432507@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/r65578f3761a89bc164e8964acd5d913b9f8fd997967b195a89a97ca3@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/r7641ee788e1eb1be4bb206a7d15f8a64ec6ef23e5ec6132d5a567695@%3Cnotifications.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r7e5c10534ed06bf805473ac85e8412fe3908a8fa4cabf5027bf11220@%3Cdev.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r7e739f2961753af95e2a3a637828fb88bfca68e5d6b0221d483a9ee5@%3Cnotifications.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r8c001b9a95c0bbec06f4457721edd94935a55932e64b82cc5582b846@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/r8e96c340004b7898cad3204ea51280ef6e4b553a684e1452bf1b18b1@%3Cjira.kafka.apache.org%3E", "https://lists.apache.org/thread.html/r9776e71e3c67c5d13a91c1eba0dc025b48b802eb7561cc6956d6961c@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/r9a79175c393d14d760a0ae3731b4a873230a16ef321aa9ca48a810cd@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/ra051e07a0eea4943fa104247e69596f094951f51512d42c924e86c75@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/ra632b329b2ae2324fabbad5da204c4ec2e171ff60348ec4ba698fd40@%3Cissues.hive.apache.org%3E", "https://lists.apache.org/thread.html/rbc45eb0f53fd6242af3e666c2189464f848a851d408289840cecc6e3@%3Ccommits.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rbc7642b9800249553f13457e46b813bea1aec99d2bc9106510e00ff3@%3Ctorque-dev.db.apache.org%3E", "https://lists.apache.org/thread.html/rc2dbc4633a6eea1fcbce6831876cfa17b73759a98c65326d1896cb1a@%3Ctorque-dev.db.apache.org%3E", "https://lists.apache.org/thread.html/rc6b81c013618d1de1b5d6b8c1088aaf87b4bacc10c2371f15a566701@%3Cnotifications.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rd0e44e8ef71eeaaa3cf3d1b8b41eb25894372e2995ec908ce7624d26@%3Ccommits.pulsar.apache.org%3E", "https://lists.apache.org/thread.html/rd55f65c6822ff235eda435d31488cfbb9aa7055cdf47481ebee777cc@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rd5d58088812cf8e677d99b07f73c654014c524c94e7fedbdee047604@%3Ctorque-dev.db.apache.org%3E", "https://lists.apache.org/thread.html/rd8e87c4d69df335d0ba7d815b63be8bd8a6352f429765c52eb07ddac@%3Cissues.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/re024d86dffa72ad800f2848d0c77ed93f0b78ee808350b477a6ed987@%3Cgitbox.hive.apache.org%3E", "https://lists.apache.org/thread.html/rec34b1cccf907898e7cb36051ffac3ccf1ea89d0b261a2a3b3fb267f@%3Ccommits.zookeeper.apache.org%3E", "https://lists.apache.org/thread.html/rf1c2a81a08034c688b8f15cf58a4cfab322d00002ca46d20133bee20@%3Cdev.kafka.apache.org%3E", "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", "https://lists.debian.org/debian-lts-announce/2021/12/msg00017.html", "https://nvd.nist.gov/vuln/detail/CVE-2020-9488", "https://security.netapp.com/advisory/ntap-20200504-0003/", "https://www.cve.org/CVERecord?id=CVE-2020-9488", "https://www.debian.org/security/2021/dsa-5020", "https://www.openwall.com/lists/oss-security/2020/04/25/1", "https://www.oracle.com/security-alerts/cpuApr2021.html", "https://www.oracle.com/security-alerts/cpuapr2022.html", "https://www.oracle.com/security-alerts/cpujan2021.html", "https://www.oracle.com/security-alerts/cpujul2020.html", "https://www.oracle.com/security-alerts/cpuoct2020.html", "https://www.oracle.com/security-alerts/cpuoct2021.html" ], "PublishedDate": "2020-04-27T16:15:00Z", "LastModifiedDate": "2022-05-12T15:00:00Z" } ] } ] } ================================================ FILE: regoservice/testdata/inputs/trivy-operator-input.json ================================================ { "apiVersion": "aquasecurity.github.io/v1alpha1", "kind": "VulnerabilityReport", "metadata": { "name": "replicaset-nginx-6d4cf56db6-nginx", "namespace": "default", "labels": { "trivy-operator.container.name": "nginx", "trivy-operator.resource.kind": "ReplicaSet", "trivy-operator.resource.name": "nginx-6d4cf56db6", "trivy-operator.resource.namespace": "default", "resource-spec-hash": "7cb64cb677" }, "uid": "8aa1a7cb-a319-4b93-850d-5a67827dfbbf", "ownerReferences": [ { "apiVersion": "apps/v1", "blockOwnerDeletion": false, "controller": true, "kind": "ReplicaSet", "name": "nginx-6d4cf56db6", "uid": "aa345200-cf24-443a-8f11-ddb438ff8659" } ] }, "report": { "artifact": { "repository": "library/nginx", "tag": "1.16" }, "registry": { "server": "index.docker.io" }, "scanner": { "name": "Trivy", "vendor": "Aqua Security", "version": "0.30.0" }, "summary": { "criticalCount": 2, "highCount": 0, "lowCount": 0, "mediumCount": 0, "unknownCount": 0 }, "vulnerabilities": [ { "fixedVersion": "0.9.1-2+deb10u1", "installedVersion": "0.9.1-2", "links": [], "primaryLink": "https://avd.aquasec.com/nvd/cve-2019-20367", "resource": "libbsd0", "score": 9.1, "severity": "CRITICAL", "target": "library/nginx:1.21.6", "title": "", "vulnerabilityID": "CVE-2019-20367" }, { "fixedVersion": "", "installedVersion": "0.6.1-2", "links": [], "primaryLink": "https://avd.aquasec.com/nvd/cve-2018-25009", "resource": "libwebp6", "score": 6.1, "severity": "HIGH", "target": "library/nginx:1.16", "title": "libwebp: out-of-bounds read in WebPMuxCreateInternal", "vulnerabilityID": "CVE-2018-25009" }, { "fixedVersion": "", "installedVersion": "0.6.1-2", "links": [], "primaryLink": "https://avd.aquasec.com/nvd/cve-2018-25009", "resource": "libwebp3", "score": 3.2, "severity": "MEDIUM", "target": "library/nginx:1.16", "title": "libwebp: out-of-bounds read in WebPMuxCreateInternal", "vulnerabilityID": "CVE-2018-25010" }, { "fixedVersion": "", "installedVersion": "0.6.1-2", "links": [], "primaryLink": "https://avd.aquasec.com/nvd/cve-2018-25009", "resource": "libwebp4", "score": 1.1, "severity": "LOW", "target": "library/nginx:1.16", "title": "libwebp: out-of-bounds read in WebPMuxCreateInternal", "vulnerabilityID": "CVE-2018-25011" }, { "fixedVersion": "", "installedVersion": "0.6.1-2", "links": [], "primaryLink": "https://avd.aquasec.com/nvd/cve-2018-25009", "resource": "libwebp5", "score": 0, "severity": "UNKNOWN", "target": "library/nginx:1.16", "title": "libwebp: out-of-bounds read in WebPMuxCreateInternal", "vulnerabilityID": "CVE-2018-25012" } ] } } ================================================ FILE: regoservice/testdata/templates/common/common.rego ================================================ package postee flat_array(a) = o { o:=[item | item:=a[_][_] ] } ================================================ FILE: regoservice/testdata/templates/html-with-complex-pkg.rego ================================================ package rego2.html title:="Audit event received" result:=sprintf("Audit event received from %s", [input.user]) url:="Audit-registry-received/Audit-image-received" ================================================ FILE: regoservice/testdata/templates/html.rego ================================================ package rego1.html title:="Audit event received" result:=sprintf("Audit event received from %s", [input.user]) url:="Audit-registry-received/Audit-image-received" ================================================ FILE: regoservice/testdata/templates/invalid.rego ================================================ package rego1.invalid default input = false ================================================ FILE: regoservice/testdata/templates/json-without-url.rego ================================================ package rego1.json.without.url title:="Audit event received" result:={ "assignee": input.user } ================================================ FILE: regoservice/testdata/templates/json.rego ================================================ package rego1.json title:="Audit event received" result:={ "assignee": input.user } url:="Audit-registry-received/Audit-image-received" ================================================ FILE: regoservice/testdata/templates/without-any-expression.rego ================================================ package rego1.without.any.expression ================================================ FILE: regoservice/testdata/templates/without-result.rego ================================================ package rego1.without.result ttle:="Audit event received" ================================================ FILE: router/anonymizeSettings_test.go ================================================ package router import "testing" func TestAnonymizeSettings(t *testing.T) { tests := []struct { original *ActionSettings expected *ActionSettings }{{ &ActionSettings{ User: "admin", }, &ActionSettings{ User: "", }, }, { &ActionSettings{ User: "", }, &ActionSettings{ User: "", }, }, { &ActionSettings{ Password: "secret", }, &ActionSettings{ Password: "", }, }, { &ActionSettings{ Url: "http://localhost", }, &ActionSettings{ Url: "", }, }, } for _, test := range tests { anonymized := anonymizeSettings(test.original) if anonymized == test.original { t.Errorf("Anonymized settings weren't cloned") } if anonymized.User != test.expected.User { t.Errorf("Settings anonymization is incorrect: expected User %s, got %s", test.expected.User, anonymized.User) } if anonymized.Password != test.expected.Password { t.Errorf("Settings anonymization is incorrect: expected Password %s, got %s", test.expected.Password, anonymized.Password) } if anonymized.Url != test.expected.Url { t.Errorf("Settings anonymization is incorrect: expected Url %s, got %s", test.expected.Url, anonymized.Url) } } } ================================================ FILE: router/anonymizer.go ================================================ package router import "reflect" func anonymizeSettings(settings *ActionSettings) *ActionSettings { fieldsToAnonymize := [...]string{ "User", "Password", "Url", "InstanceName", } copyToAnonymize := *settings for _, key := range fieldsToAnonymize { r := reflect.ValueOf(©ToAnonymize) v := reflect.Indirect(r).FieldByName(key) prop := v.String() if prop != "" { v.SetString(AnonymizeReplacement) } } return ©ToAnonymize } ================================================ FILE: router/builders.go ================================================ package router import ( "fmt" "net/http" "net/url" "strings" "time" "github.com/aquasecurity/postee/v2/actions" ) func buildStdoutAction(sourceSettings *ActionSettings) *actions.StdoutAction { return &actions.StdoutAction{Name: sourceSettings.Name} } func buildSplunkAction(sourceSettings *ActionSettings) *actions.SplunkAction { return &actions.SplunkAction{ Name: sourceSettings.Name, Url: sourceSettings.Url, Token: sourceSettings.Token, EventLimit: sourceSettings.SizeLimit, TlsVerify: sourceSettings.TlsVerify, } } func buildWebhookAction(sourceSettings *ActionSettings) *actions.WebhookAction { return &actions.WebhookAction{ Name: sourceSettings.Name, Url: sourceSettings.Url, Timeout: sourceSettings.Timeout, } } func buildTeamsAction(sourceSettings *ActionSettings, aquaServer string) *actions.TeamsAction { return &actions.TeamsAction{ Name: sourceSettings.Name, AquaServer: aquaServer, Webhook: sourceSettings.Url, } } func buildServiceNow(sourceSettings *ActionSettings) *actions.ServiceNowAction { serviceNow := &actions.ServiceNowAction{ Name: sourceSettings.Name, User: sourceSettings.User, Password: sourceSettings.Password, Table: sourceSettings.BoardName, Instance: sourceSettings.InstanceName, } if len(serviceNow.Table) == 0 { serviceNow.Table = ServiceNowTableDefault } return serviceNow } func buildSlackAction(sourceSettings *ActionSettings, aqua string) *actions.SlackAction { return &actions.SlackAction{ Name: sourceSettings.Name, AquaServer: aqua, Url: sourceSettings.Url, } } func buildEmailAction(sourceSettings *ActionSettings) *actions.EmailAction { return &actions.EmailAction{ Name: sourceSettings.Name, User: sourceSettings.User, Password: sourceSettings.Password, Host: sourceSettings.Host, Port: sourceSettings.Port, Sender: sourceSettings.Sender, Recipients: sourceSettings.Recipients, ClientHostName: sourceSettings.ClientHostName, UseMX: sourceSettings.UseMX, } } func buildNexusIqAction(sourceSettings *ActionSettings) *actions.NexusIqAction { return &actions.NexusIqAction{ Name: sourceSettings.Name, Url: sourceSettings.Url, User: sourceSettings.User, Password: sourceSettings.Password, OrganizationId: sourceSettings.OrganizationId, } } func buildDependencyTrackAction(sourceSettings *ActionSettings) *actions.DependencyTrackAction { return &actions.DependencyTrackAction{ Name: sourceSettings.Name, Url: sourceSettings.Url, APIKey: sourceSettings.DependencyTrackAPIKey, } } func buildOpsGenieAction(sourceSettings *ActionSettings) *actions.OpsGenieAction { return &actions.OpsGenieAction{ Name: sourceSettings.Name, User: sourceSettings.User, APIKey: sourceSettings.Token, Responders: sourceSettings.Assignee, VisibleTo: sourceSettings.Recipients, PrioritySource: sourceSettings.Priority, Tags: sourceSettings.Tags, Alias: sourceSettings.Alias, Entity: sourceSettings.Entity, } } func buildJiraAction(sourceSettings *ActionSettings) *actions.JiraAPI { jiraApi := &actions.JiraAPI{ Name: sourceSettings.Name, Url: sourceSettings.Url, User: sourceSettings.User, Password: sourceSettings.Password, Token: sourceSettings.Token, TlsVerify: sourceSettings.TlsVerify, Issuetype: sourceSettings.IssueType, ProjectKey: strings.ToUpper(sourceSettings.ProjectKey), Priority: sourceSettings.Priority, Assignee: sourceSettings.Assignee, FixVersions: sourceSettings.FixVersions, AffectsVersions: sourceSettings.AffectsVersions, Labels: sourceSettings.Labels, Unknowns: sourceSettings.Unknowns, SprintName: sourceSettings.Sprint, SprintId: actions.NotConfiguredSprintId, BoardName: sourceSettings.BoardName, } if len(jiraApi.Assignee) == 0 { jiraApi.Assignee = []string{jiraApi.User} } return jiraApi } func buildExecAction(sourceSettings *ActionSettings) (*actions.ExecClient, error) { if len(sourceSettings.InputFile) <= 0 && len(sourceSettings.ExecScript) <= 0 { return nil, fmt.Errorf("exec action requires either input-file or exec-script to be set") } if len(sourceSettings.InputFile) > 0 && len(sourceSettings.ExecScript) > 0 { return nil, fmt.Errorf("exec action only takes either input-file or exec-script, not both") } ec := &actions.ExecClient{ Name: sourceSettings.Name, Env: sourceSettings.Env, } if len(sourceSettings.InputFile) > 0 { ec.InputFile = sourceSettings.InputFile } if len(sourceSettings.ExecScript) > 0 { ec.ExecScript = sourceSettings.ExecScript } return ec, nil } func buildHTTPAction(sourceSettings *ActionSettings) (*actions.HTTPClient, error) { if len(sourceSettings.Method) <= 0 { return nil, fmt.Errorf("http action requires a method to be specified") } if len(sourceSettings.BodyFile) > 0 && len(sourceSettings.BodyContent) > 0 { return nil, fmt.Errorf("http action requires only supports body-file or body-content, not both") } var duration time.Duration if len(sourceSettings.Timeout) > 0 { var err error duration, err = time.ParseDuration(sourceSettings.Timeout) if err != nil { return nil, fmt.Errorf("invalid duration specified: %w", err) } } else { duration = time.Second * 5 } reqUrl, err := url.Parse(sourceSettings.Url) if err != nil { return nil, fmt.Errorf("error building HTTP url: %w", err) } hc := &actions.HTTPClient{ Name: sourceSettings.Name, Client: http.Client{Timeout: duration}, URL: reqUrl, Method: strings.ToUpper(sourceSettings.Method), Headers: sourceSettings.Headers, } if len(sourceSettings.BodyFile) > 0 { hc.BodyFile = sourceSettings.BodyFile } if len(sourceSettings.BodyContent) > 0 { hc.BodyContent = sourceSettings.BodyContent } return hc, nil } func buildKubernetesAction(sourceSettings *ActionSettings) (*actions.KubernetesClient, error) { if !actions.IsK8s() { if sourceSettings.KubeConfigFile == "" { return nil, fmt.Errorf("kubernetes config file needs to be set in config yaml") } } if sourceSettings.KubeNamespace == "" { return nil, fmt.Errorf("kubernetes namespace needs to be set in config yaml") } return &actions.KubernetesClient{ Name: sourceSettings.Name, KubeNamespace: sourceSettings.KubeNamespace, KubeConfigFile: sourceSettings.KubeConfigFile, KubeLabelSelector: sourceSettings.KubeLabelSelector, KubeActions: sourceSettings.KubeActions, }, nil } func buildDockerAction(sourceSettings *ActionSettings) (*actions.DockerClient, error) { if len(sourceSettings.DockerImageName) < 0 { return nil, fmt.Errorf("docker action requires an image name") } return &actions.DockerClient{ Name: sourceSettings.Name, ImageName: sourceSettings.DockerImageName, Cmd: sourceSettings.DockerCmd, Volumes: sourceSettings.DockerVolumes, Env: sourceSettings.DockerEnv, Network: sourceSettings.DockerNetwork, }, nil } func buildAWSSecurityHubAction(sourceSettings *ActionSettings) (*actions.AWSSecurityHubClient, error) { return &actions.AWSSecurityHubClient{Name: sourceSettings.Name}, nil } func buildPagerdutyAction(sourceSettings *ActionSettings) (*actions.PagerdutyClient, error) { return &actions.PagerdutyClient{ Name: sourceSettings.Name, AuthToken: sourceSettings.PagerdutyAuthToken, RoutingKey: sourceSettings.PagerdutyRoutingKey, }, nil } ================================================ FILE: router/goldens/kube-config.sample ================================================ apiVersion: v1 clusters: - cluster: server: https://kubernetes.docker.internal:6443 name: foo contexts: - context: cluster: foo user: foo name: foo current-context: foo kind: Config preferences: {} ================================================ FILE: router/goldens/sample.cfg ================================================ name: Postee Controller Runner Demo aqua-server: # URL of Aqua Server for links. E.g. https://myserver.aquasec.com max-db-size: 1000MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited db-verify-interval: 1 # How often to check the DB size. By default, Postee checks every 1 hour # Routes are used to define how to handle an incoming message routes: - name: stdout actions: [ stdout ] template: raw-json - name: controller-only-route input: contains(input.image, "alpine") actions: [my-http-post-from-controller] template: raw-json - name: runner-only-route input: contains(input.SigMetadata.ID, "TRC-1") serialize-actions: true actions: [my-exec-from-runner, my-http-post-from-runner] template: raw-json - name: controller-runner-route input: contains(input.SigMetadata.ID, "TRC-2") serialize-actions: true # Cannot be strictly guaranteed as executions happen independently on runner/controller actions: [my-exec-from-runner, my-http-post-from-runner, my-http-post-from-controller] template: raw-json # Templates are used to format a message templates: - name: raw-json # route message "As Is" to external webhook rego-package: postee.rawmessage.json # Outputs are target services that should consume the messages actions: - name: stdout type: stdout enable: true - name: my-http-post-from-controller type: http enable: true url: "https://webhook.site/" # Required. URL of the HTTP Request method: POST # Required. Method to use. CONNECT is not supported at this time headers: # Optional. Headers to pass in for the request. "Foo": [ "bar" ] timeout: 10s # Optional. Timeout value in XX(s,m,h) body-content: | # Optional. Body inline content of the HTTP request This is an example of a inline body Input Image: event.input.image - name: my-exec-from-runner runs-on: "postee-runner-1" type: exec enable: true env: ["MY_ENV_VAR=foo_bar_baz", "MY_KEY=secret"] # Optional. Any environment variables to pass in exec-script: | # Specify the script to run #!/bin/sh echo $POSTEE_EVENT echo "this is hello from postee" - name: my-http-post-from-runner runs-on: "postee-runner-1" type: http enable: true url: "https://webhook.site/" # Required. URL of the HTTP Request method: POST # Required. Method to use. CONNECT is not supported at this time body-content: | # Optional. Body inline content of the HTTP request This is an another example of a inline body Event ID: event.input.SigMetadata.ID ================================================ FILE: router/goldens/test.txt ================================================ foo bar baz ================================================ FILE: router/initoutputs_test.go ================================================ package router import ( "fmt" "reflect" "testing" "github.com/stretchr/testify/assert" ) func TestBuildAndInitOtpt(t *testing.T) { tests := []struct { caseDesc string actionSettings ActionSettings expctdProps map[string]interface{} shouldFail bool expectedActionClass string }{ { "Default Stdout Action", ActionSettings{ Name: "stdout", Type: "stdout", Enable: true, }, map[string]interface{}{ "Name": "stdout", }, false, "*actions.StdoutAction", }, { "Simple Slack", ActionSettings{ Name: "my-slack", Type: "slack", Enable: true, Url: "https://hooks.slack.com/services/TT/BBB/WWWW", }, map[string]interface{}{ "Url": "https://hooks.slack.com/services/TT/BBB/WWWW", "Name": "my-slack", }, false, "*actions.SlackAction", }, { "Simple Email action", ActionSettings{ User: "EmailUser", Password: "pAsSw0rD", Host: "smtp.gmail.com", Name: "my-email", Type: "email", Port: 587, Sender: "google@gmail.com", Recipients: []string{"r1@gmail.com"}, }, map[string]interface{}{ "User": "EmailUser", "Password": "pAsSw0rD", "Host": "smtp.gmail.com", "Port": 587, "Sender": "google@gmail.com", "Recipients": []string{"r1@gmail.com"}, }, false, "*actions.EmailAction", }, { "Simple Nexus IQ action", ActionSettings{ Url: "http://localhost:8070", User: "admin", Password: "admin123", Name: "my-nexus", Type: "nexusIq", OrganizationId: "222de33e8005408a844c12eab952c9b0", }, map[string]interface{}{ "Url": "http://localhost:8070", "User": "admin", "Password": "admin123", "OrganizationId": "222de33e8005408a844c12eab952c9b0", }, false, "*actions.NexusIqAction", }, { "Simple Dependency Track action", ActionSettings{ Url: "http://localhost:8080", Name: "my-dependencytrack", Type: "dependencytrack", DependencyTrackAPIKey: "api-key", }, map[string]interface{}{ "Url": "http://localhost:8080", "APIKey": "api-key", }, false, "*actions.DependencyTrackAction", }, { "Simple Jira action", ActionSettings{ Url: "localhost:2990", User: "admin", Password: "admin", Name: "my-jira", Type: "jira", ProjectKey: "PK", IssueType: "IssueType", Priority: "Priority", Assignee: []string{"Assignee"}, }, map[string]interface{}{ "Url": "localhost:2990", "User": "admin", "Password": "admin", "ProjectKey": "PK", "Issuetype": "IssueType", "Priority": "Priority", "Assignee": []string{"Assignee"}, }, false, "*actions.JiraAPI", }, { "Jira action without credentials", ActionSettings{ Url: "localhost:2990", Name: "my-jira", Type: "jira", ProjectKey: "PK", IssueType: "IssueType", Priority: "Priority", Assignee: []string{"Assignee"}, }, map[string]interface{}{}, true, "", }, { "Jira action without password", ActionSettings{ Url: "localhost:2990", User: "admin", Name: "my-jira", Type: "jira", ProjectKey: "PK", IssueType: "IssueType", Priority: "Priority", Assignee: []string{"Assignee"}, }, map[string]interface{}{}, true, "", }, { "Jira action with missed type", ActionSettings{ Url: "localhost:2990", User: "admin", Name: "my-jira", ProjectKey: "PK", IssueType: "IssueType", Priority: "Priority", Assignee: []string{"Assignee"}, }, map[string]interface{}{}, true, "", }, { "Jira Action with some default values", ActionSettings{ Url: "localhost:2990", Name: "my-jira-with-defaults", Type: "jira", User: "admin", Password: "admin", ProjectKey: "PK", }, map[string]interface{}{ "Url": "localhost:2990", "User": "admin", "Password": "admin", "ProjectKey": "PK", "Assignee": []string{"admin"}, }, false, "*actions.JiraAPI", }, { "Simple webhook action", ActionSettings{ Url: "http://localhost:8080", Name: "my-webhook", Type: "webhook", }, map[string]interface{}{ "Url": "http://localhost:8080", }, false, "*actions.WebhookAction", }, { "Simple ServiceNow action", ActionSettings{ Name: "my-servicenow", Type: "serviceNow", User: "admin", Password: "secret", InstanceName: "dev108148", BoardName: "incindent", }, map[string]interface{}{ "User": "admin", "Password": "secret", "Instance": "dev108148", "Table": "incindent", }, false, "*actions.ServiceNowAction", }, { "ServiceNow action without BoardName", ActionSettings{ Name: "my-servicenow", Type: "serviceNow", User: "admin", Password: "secret", InstanceName: "dev108148", }, map[string]interface{}{ "User": "admin", "Password": "secret", "Instance": "dev108148", "Table": ServiceNowTableDefault, }, false, "*actions.ServiceNowAction", }, { "Simple Teams action", ActionSettings{ Url: "https://outlook.office.com/webhook/ABCD", Name: "my-teams", Type: "teams", }, map[string]interface{}{ "Webhook": "https://outlook.office.com/webhook/ABCD", }, false, "*actions.TeamsAction", }, { "Simple Splunk action", ActionSettings{ Url: "http://localhost:8088", Name: "my-splunk", Type: "splunk", Token: "test_token_for_splunk", }, map[string]interface{}{ "Url": "http://localhost:8088", "Name": "my-splunk", "Token": "test_token_for_splunk", }, false, "*actions.SplunkAction", }, { "HTTP Action action, with a timeout & body file specified", ActionSettings{ Method: "POST", Timeout: "10s", Url: "https://foo.bar.com", Name: "my-http-action", Type: "http", BodyFile: "goldens/test.txt", }, map[string]interface{}{ "Name": "my-http-action", "Method": "POST", "BodyFile": "goldens/test.txt", }, false, "*actions.HTTPClient", }, { "HTTP Action action, with a timeout & body content specified", ActionSettings{ Method: "POST", Timeout: "10s", Url: "https://foo.bar.com", Name: "my-http-action", Type: "http", BodyContent: "foo bar baz body", }, map[string]interface{}{ "Name": "my-http-action", "Method": "POST", "BodyContent": "foo bar baz body", }, false, "*actions.HTTPClient", }, { "HTTP Action action, with a timeout & both body content and file specified", ActionSettings{ Method: "POST", Timeout: "10s", Url: "https://foo.bar.com", Name: "my-http-action", Type: "http", BodyFile: "goldens/test.txt", BodyContent: "foo bar baz body", }, map[string]interface{}{}, true, "", }, { "HTTP Action action, with no method specified", ActionSettings{ Url: "https://foo.bar.com", Name: "my-http-action", Type: "http", }, map[string]interface{}{}, true, "", }, { "HTTP Action action, with invalid url specified", ActionSettings{ Method: "get", Url: "http://[fe80::1%en0]/", Name: "my-http-action", Type: "http", }, map[string]interface{}{}, true, "", }, { "HTTP Action action, with a invalid timeout", ActionSettings{ Method: "GET", Timeout: "ten seconds", Type: "http", }, map[string]interface{}{}, true, "", }, {"Exec Action action, with input-file config", ActionSettings{ Name: "my-exec-action", Env: []string{"foo=bar"}, InputFile: "goldens/test.txt", Type: "exec", }, map[string]interface{}{ "Name": "my-exec-action", "InputFile": "goldens/test.txt", }, false, "*actions.ExecClient", }, {"Exec Action action, with exec-script config", ActionSettings{ Name: "my-exec-action", Env: []string{"foo=bar"}, ExecScript: `#!/bin/sh echo "foo bar"`, Type: "exec", }, map[string]interface{}{ "Name": "my-exec-action", "ExecScript": `#!/bin/sh echo "foo bar"`, }, false, "*actions.ExecClient", }, {"Exec Action action, with invalid config (both file and script)", ActionSettings{ Name: "my-exec-action", Env: []string{"foo=bar"}, InputFile: "goldens/test.txt", ExecScript: `#!/bin/sh echo "foo bar"`, Type: "exec", }, map[string]interface{}{}, true, "", }, {"Exec Action action, with invalid config (no file nor script)", ActionSettings{ Name: "my-exec-output", Env: []string{"foo=bar"}, Type: "exec", }, map[string]interface{}{}, true, "", }, { "Kubernetes Action, happy path", ActionSettings{ Name: "my-k8s-output", Type: "kubernetes", KubeNamespace: "default", KubeConfigFile: "goldens/kube-config.sample", KubeLabelSelector: "app=foobar", KubeActions: map[string]map[string]string{ "labels": {"foo-label": "bar-value"}, "annotations": {"foo-annotation": "bar-value"}, }, }, map[string]interface{}{ "Name": "my-k8s-output", "KubeNamespace": "default", "KubeConfigFile": "goldens/kube-config.sample", "KubeLabelSelector": "app=foobar", "KubeActions": map[string]map[string]string{ "labels": {"foo-label": "bar-value"}, "annotations": {"foo-annotation": "bar-value"}, }, }, false, "*actions.KubernetesClient", }, { "Kubernetes Action, sad path, no kube-config", ActionSettings{ Name: "my-k8s-output", Type: "kubernetes", KubeNamespace: "default", KubeLabelSelector: "app=foobar", }, map[string]interface{}{}, true, "", }, { "Kubernetes Action, sad path, no kube namespace", ActionSettings{ Name: "my-k8s-output", Type: "kubernetes", KubeConfigFile: "goldens/kube-config.sample", KubeLabelSelector: "app=foobar", }, map[string]interface{}{}, true, "", }, } for _, test := range tests { t.Run(test.caseDesc, func(t *testing.T) { o := BuildAndInitOtpt(&test.actionSettings, "") if test.shouldFail && o != nil { t.Fatalf("No output expected for %s test case but was %s", test.caseDesc, o) } else if !test.shouldFail && o == nil { t.Fatalf("Not expected output returned for %s test case", test.caseDesc) } actualActionCls := fmt.Sprintf("%T", o) if actualActionCls != test.expectedActionClass { t.Errorf("[%s] Incorrect output type, expected %s, got %s", test.caseDesc, test.expectedActionClass, actualActionCls) } for key, prop := range test.expctdProps { //t.Logf("key %s\n", key) r := reflect.ValueOf(o) v := reflect.Indirect(r).FieldByName(key) if !v.IsValid() { t.Errorf("Property %s is not found", key) continue } mbStringSlice, ok := prop.([]string) if ok { vSlice, ok := v.Interface().([]string) if !ok { t.Errorf("Invalid type of property %s, expected []string, got %T", key, v.Interface()) } if len(mbStringSlice) == len(vSlice) { for i := range mbStringSlice { if mbStringSlice[i] != vSlice[i] { t.Errorf("Invalid property %s, expected: %q, got: %q", key, mbStringSlice[i], vSlice[i]) } } } else { t.Errorf("Wrong size of %s, expected: %d, got: %d", key, len(mbStringSlice), len(vSlice)) } } else { assert.EqualValues(t, prop, v.Interface()) } } }) } } ================================================ FILE: router/inittemplate_test.go ================================================ package router import ( "bytes" "errors" "fmt" "io/ioutil" "net/http" "os" "testing" ) var ( regoRule = "package postee.slack" ) func TestInitTemplate(t *testing.T) { savedGetHttpClient := getHttpClient getHttpClient = getMockedHttpClient defaultRegoFolder := "rego-templates" commonRegoFolder := defaultRegoFolder + "/common" testRego := defaultRegoFolder + "/rego1.rego" err := os.Mkdir(defaultRegoFolder, 0777) if err != nil { t.Fatalf("Can't create rego folder: %v", err) } err = os.Mkdir(commonRegoFolder, 0777) if err != nil { t.Fatalf("Can't create rego folder: %v", err) } err = ioutil.WriteFile(testRego, []byte(regoRule), 0644) if err != nil { t.Fatalf("Can't write rego: %v", err) } defer func() { os.Remove(testRego) os.Remove(commonRegoFolder) os.Remove(defaultRegoFolder) getHttpClient = savedGetHttpClient }() tests := []struct { template *Template caseDesc string expectedCls string shouldReturnError bool }{ { template: &Template{ Name: "legacy-html", LegacyScanRenderer: "html", }, caseDesc: "Legacy mode test", expectedCls: "*formatting.legacyScnEvaluator", }, { template: &Template{ Name: "built-in", RegoPackage: "postee.slack", }, caseDesc: "Built-in rego package", expectedCls: "*regoservice.regoEvaluator", }, { template: &Template{ Name: "from-url", Url: "http://localhost/slack.rego", }, caseDesc: "Loading rego from url", expectedCls: "*regoservice.regoEvaluator", }, { template: &Template{ Name: "not-found", Url: "http://localhost/wrong.rego", }, caseDesc: "Loading rego from not existing url", expectedCls: "*regoservice.regoEvaluator", shouldReturnError: true, }, { template: &Template{ Name: "from-invalid-url", Url: "invalid-url", }, caseDesc: "Loading rego from invalid url", expectedCls: "*regoservice.regoEvaluator", shouldReturnError: true, }, { template: &Template{ Name: "inline", Body: "package postee.inline", }, caseDesc: "Loading rego from yaml config", expectedCls: "*regoservice.regoEvaluator", }, } for _, test := range tests { doInitTemplate(t, test.caseDesc, test.template, test.expectedCls, test.shouldReturnError) } } func doInitTemplate(t *testing.T, caseDesc string, template *Template, expectedCls string, shouldReturnError bool) { demoCtx := Instance() err := demoCtx.initTemplate(template) if err != nil && !shouldReturnError { t.Fatalf("[%s] Unexpected error: %v", caseDesc, err) } if err == nil && shouldReturnError { t.Fatalf("Test case [%s] should return an error", caseDesc) } if shouldReturnError { return } initialized, ok := demoCtx.templates[template.Name] if !ok { t.Fatalf("[%s] template %s is not initialized", caseDesc, template.Name) } actualCls := fmt.Sprintf("%T", initialized) if actualCls != expectedCls { t.Errorf("[%s] Unexpected type of input evaluator. Expected %s, got %s \n", caseDesc, expectedCls, actualCls) } } //stuff for mocking http requests goes below func getMockedHttpClient() *http.Client { return NewTestClient(responseWithRego) } // RoundTripFunc type RoundTripFunc func(req *http.Request) (*http.Response, error) // RoundTrip func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { //this is kind of wrapper where original function is used in interface implementation return f(req) } //NewTestClient returns *http.Client with Transport replaced to avoid making real calls func NewTestClient(fn RoundTripFunc) *http.Client { return &http.Client{ Transport: RoundTripFunc(fn), } } func responseWithRego(req *http.Request) (*http.Response, error) { if req.URL.String() == "http://localhost/wrong.rego" { fmt.Printf("response status is %d\n", 404) return newTestResponse(404, "not found"), nil } else if req.URL.String() == "invalid-url" { return nil, errors.New("invalid url") } else { fmt.Printf("response status is %d\n", 200) return newTestResponse(200, "package custom1"), nil } } func newTestResponse(status int, response string) *http.Response { return &http.Response{ StatusCode: status, Body: ioutil.NopCloser(bytes.NewBufferString(response)), // Must be set to non-nil value or it panics Header: make(http.Header), } } ================================================ FILE: router/integrations.go ================================================ package router type ActionSettings struct { Name string `json:"name,omitempty"` Type string `json:"type,omitempty"` RunsOn string `json:"runs-on,omitempty"` Enable bool `json:"enable,omitempty"` Url string `json:"url,omitempty"` User string `json:"user,omitempty"` Password string `json:"password,omitempty"` TlsVerify bool `json:"tls-verify,omitempty"` ProjectKey string `json:"project-key,omitempty" structs:"project-key,omitempty"` IssueType string `json:"issuetype,omitempty" structs:"issuetype"` BoardName string `json:"board,omitempty" structs:"board,omitempty"` Priority string `json:"priority,omitempty"` Assignee []string `json:"assignee,omitempty"` Summary string `json:"summary,omitempty"` FixVersions []string `json:"fix-versions,omitempty"` AffectsVersions []string `json:"affects-versions,omitempty"` Labels []string `json:"labels,omitempty"` Sprint string `json:"sprint,omitempty"` Unknowns map[string]string `json:"unknowns,omitempty" structs:"unknowns,omitempty"` Host string `json:"host,omitempty"` Port int `json:"port,omitempty"` Recipients []string `json:"recipients,omitempty"` Sender string `json:"sender,omitempty"` Token string `json:"token,omitempty"` ClientHostName string `json:"client-host-name,omitempty"` UseMX bool `json:"use-mx,omitempty"` InstanceName string `json:"instance,omitempty"` SizeLimit int `json:"size-limit,omitempty"` InputFile string `json:"input-file,omitempty"` ExecScript string `json:"exec-script,omitempty"` Env []string `json:"env,omitempty"` BodyFile string `json:"body-file,omitempty"` BodyContent string `json:"body-content,omitempty"` Method string `json:"method,omitempty"` Timeout string `json:"timeout,omitempty"` Headers map[string][]string `json:"headers,omitempty"` OrganizationId string `json:"organization-id,omitempty"` KubeConfigFile string `json:"kube-config-file,omitempty"` KubeLabelSelector string `json:"kube-label-selector,omitempty"` KubeActions map[string]map[string]string `json:"kube-actions,omitempty"` KubeNamespace string `json:"kube-namespace,omitempty"` DockerImageName string `json:"docker-image-name,omitempty"` DockerNetwork string `json:"docker-network,omitempty"` DockerCmd []string `json:"docker-cmd,omitempty"` DockerVolumes map[string]string `json:"docker-volume-mounts,omitempty"` DockerEnv []string `json:"docker-env,omitempty"` Tags []string `json:"tags,omitempty"` Alias string `json:"alias,omitempty"` Entity string `json:"entity,omitempty"` PagerdutyAuthToken string `json:"pagerduty-auth-token,omitempty"` PagerdutyRoutingKey string `json:"pagerduty-routing-key,omitempty"` DependencyTrackAPIKey string `json:"dependency-track-api-key,omitempty"` } ================================================ FILE: router/loads_test.go ================================================ package router import ( "fmt" "io/ioutil" "log" "os" "testing" "time" "github.com/aquasecurity/postee/v2/actions" "github.com/aquasecurity/postee/v2/data" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/msgservice" "github.com/aquasecurity/postee/v2/routes" ) type ctxWrapper struct { instance *Router savedBaseForTicker time.Duration savedGetService func() service savedDBPath string cfgPath string defaultRegoFolder string commonRegoFolder string buff chan invctn } type invctn struct { actionCls string templateCls string routeName string } func (ctx *ctxWrapper) MsgHandling(input []byte, action actions.Action, route *routes.InputRoute, inpteval data.Inpteval, aquaServer *string) { i := invctn{ fmt.Sprintf("%T", action), fmt.Sprintf("%T", inpteval), route.Name, } ctx.buff <- i } func (ctxWrapper *ctxWrapper) setup(cfg string) { ctxWrapper.savedDBPath = dbservice.DbPath ctxWrapper.savedBaseForTicker = baseForTicker ctxWrapper.cfgPath = "cfg_test.yaml" ctxWrapper.savedGetService = getScanService ctxWrapper.buff = make(chan invctn) dbservice.DbPath = "test_webhooks.db" baseForTicker = time.Microsecond ctxWrapper.defaultRegoFolder = "rego-templates" ctxWrapper.commonRegoFolder = ctxWrapper.defaultRegoFolder + "/common" err := os.Mkdir(ctxWrapper.defaultRegoFolder, 0777) if err != nil { log.Printf("Can't create %s %v", ctxWrapper.defaultRegoFolder, err) } err = os.Mkdir(ctxWrapper.commonRegoFolder, 0777) if err != nil { log.Printf("Can't create %s %v", ctxWrapper.defaultRegoFolder, err) } getScanService = func() service { return ctxWrapper } err = ioutil.WriteFile(ctxWrapper.cfgPath, []byte(cfg), 0644) if err != nil { log.Printf("Can't write to %s", ctxWrapper.cfgPath) } ctxWrapper.instance = Instance() } func (ctxWrapper *ctxWrapper) teardown() { ctxWrapper.instance.Terminate() baseForTicker = ctxWrapper.savedBaseForTicker os.Remove(ctxWrapper.cfgPath) os.Remove(dbservice.DbPath) os.Remove(ctxWrapper.commonRegoFolder) os.Remove(ctxWrapper.defaultRegoFolder) dbservice.ChangeDbPath(ctxWrapper.savedDBPath) getScanService = ctxWrapper.savedGetService close(ctxWrapper.buff) } func (ctx *ctxWrapper) EvaluateRegoRule(r *routes.InputRoute, _ []byte) bool { if r.Name == "fail_evaluation" { return false } return true } func TestLoads(t *testing.T) { t.Skip("FIXME: this test makes an external call") cfgData := ` name: tenant aqua-server: https://demolab.aquasec.com max-db-size: 13MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited delete-old-data: 7 # delete data older than N day(s). If empty then we do not delete.d routes: - name: route1 # name must be unique input: | contains(input.image, "alpine") input.vulnerability_summary.critical >= 3 actions: ["my-slack"] # a list of integrations which will receive a scan or an audit event template: raw # a template for this route plugins: policy-show-all: true - name: route2 # name must be unique input: | contains(input.image, "alpine") actions: ["my-slack"] # a list of integrations which will receive a scan or an audit event template: raw # a template for this route plugins: policy-show-all: true templates: - name: raw body: input actions: - name: splunk type: splunk enable: true url: http://localhost:8088 token: 00aac750-a69c-4ebb-8771-41905f7369dd size-limit: 1000 - name: jira type: jira enable: true url: "https://afdesk.atlassian.net/" user: admin password: admin tls-verify: false project-key: kcv` wrap := ctxWrapper{} wrap.setup(cfgData) defer wrap.teardown() demoCtx := wrap.instance err := demoCtx.Start(wrap.cfgPath) if err != nil { t.Fatal(err) } expectedActionsCnt := 2 if len(demoCtx.actions) != expectedActionsCnt { t.Errorf("There are stopped actions\nWaited: %d\nResult: %d", expectedActionsCnt, len(demoCtx.actions)) } _, ok := demoCtx.actions["jira"] if !ok { t.Errorf("'jira' action didn't start!") } expectedSrvUrl := "https://demolab.aquasec.com/#/images/" if demoCtx.aquaServer != expectedSrvUrl { t.Errorf("Wrong init of AquaServer link.\nWait: %q\nGot: %q", expectedSrvUrl, demoCtx.aquaServer) } if _, ok := demoCtx.actions["splunk"]; !ok { t.Errorf("Action 'splunk' didn't run!") } } func TestReload(t *testing.T) { t.Skip("FIXME: this test makes an external call") cfgData := ` name: tenant aqua-server: https://demolab.aquasec.com max-db-size: 13MB # Max size of DB. pattern is used, such as "300MB" or "1GB". If empty or 0 then unlimited delete-old-data: 7 # delete data older than N day(s). If empty then we do not delete.d routes: - name: route1 # name must be unique input: | contains(input.image, "alpine") input.vulnerability_summary.critical >= 3 actions: ["my-slack"] # a list of integrations which will receive a scan or an audit event template: raw # a template for this route plugins: policy-show-all: true - name: route2 # name must be unique input: | contains(input.image, "alpine") actions: ["my-slack"] # a list of integrations which will receive a scan or an audit event template: raw # a template for this route plugins: policy-show-all: true templates: - name: raw body: input actions: - name: splunk type: splunk enable: true url: http://localhost:8088 token: 00aac750-a69c-4ebb-8771-41905f7369dd size-limit: 1000 - name: jira type: jira enable: true url: "https://afdesk.atlassian.net/" user: admin password: admin tls-verify: false project-key: kcv` extraOtptCfg := ` - name: jira2 type: jira enable: true url: "https://afdesk.atlassian.net/" user: admin password: admin tls-verify: false project-key: kcv` wrap := ctxWrapper{} wrap.setup(cfgData) defer wrap.teardown() demoCtx := wrap.instance errStart := demoCtx.Start(wrap.cfgPath) if errStart != nil { t.Fatal(errStart) } expectedActionsCnt := 2 if len(demoCtx.actions) != expectedActionsCnt { t.Errorf("There are stopped actions\nWaited: %d\nResult: %d", expectedActionsCnt, len(demoCtx.actions)) } f, err := os.OpenFile(wrap.cfgPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { t.Errorf("Can't open config %v\n", err) } defer f.Close() if _, err := f.WriteString(extraOtptCfg); err != nil { t.Errorf("Can't update config %v\n", err) } demoCtx.ReloadConfig() expectedActionsAfterReload := 3 if len(demoCtx.actions) != expectedActionsAfterReload { t.Errorf("There are stopped actions\nWaited: %d\nResult: %d", expectedActionsAfterReload, len(demoCtx.actions)) } } func TestServiceGetters(t *testing.T) { scanner := getScanService() if _, ok := scanner.(*msgservice.MsgService); !ok { t.Error("getScanService() doesn't return an instance of scanservice.ScanService") } } ================================================ FILE: router/parsecfg.go ================================================ package router import ( "bytes" "io/ioutil" "log" "github.com/ghodss/yaml" ) const ( v1Marker = "- type: common" v1Warning = ` @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ Options supported only in Postee V1 are found in %s. Please make sure app is configured correctly! See https://github.com/aquasecurity/postee/blob/main/README.md for the details. @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ ` ) func Parsev2cfg(cfgpath string) (*TenantSettings, error) { data, err := ioutil.ReadFile(cfgpath) if err != nil { log.Printf("Failed to open file %s, %s", cfgpath, err) return nil, err } checkV1Cfg(data, cfgpath) tenant := &TenantSettings{} err = yaml.Unmarshal(data, tenant) if err != nil { log.Printf("Failed yaml.Unmarshal, %s", err) return nil, err } return tenant, nil } func checkV1Cfg(data []byte, cfgpath string) { if bytes.Index(data, []byte(v1Marker)) > -1 { log.Printf(v1Warning, cfgpath) } } ================================================ FILE: router/parsecfg_test.go ================================================ package router import ( "io/ioutil" "os" "testing" ) func TestParseCfgWithInvalidFilename(t *testing.T) { invalidfn := "not-a-cfg.yaml" _, err := Parsev2cfg(invalidfn) if err == nil { t.Errorf("Error is expected") } } func TestParseCfgWithInvalidYaml(t *testing.T) { cfgfn := "cfg.yaml" invalidYaml := ` playing_song_artist: Playing song, {{ song_name }} by {{ artist }} playing_playlist: {{ action }} playlist {{ playlist_name }}` defer func() { os.Remove(cfgfn) }() errWriteFile := ioutil.WriteFile(cfgfn, []byte(invalidYaml), 0644) if errWriteFile != nil { t.Errorf("Error in WriteFile: %s", errWriteFile) } _, err := Parsev2cfg(cfgfn) if err == nil { t.Errorf("Error is expected") } } ================================================ FILE: router/routehandling_test.go ================================================ package router import ( "testing" "time" ) var ( singleRoute string = ` Name: tenant routes: - name: route1 actions: ["my-slack"] template: raw plugins: Policy-Show-All: true templates: - name: raw body: | package postee result:=input actions: - name: my-slack type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/TTT` noAssociatedAction string = ` Name: tenant routes: - name: route1 template: raw plugins: Policy-Show-All: true templates: - name: raw body: | package postee result:=input actions: - name: my-slack type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/TTT` twoRoutes string = ` Name: tenant routes: - name: route1 actions: ["my-slack"] template: raw plugins: Policy-Show-All: true - name: route2 actions: ["my-slack"] template: raw plugins: Policy-Show-All: true templates: - name: raw body: | package postee result:=input actions: - name: my-slack type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/TTT` twoActions string = ` Name: tenant routes: - name: route1 actions: ["my-slack", "my-slack2"] template: raw plugins: Policy-Show-All: true templates: - name: raw body: | package postee result:=input actions: - name: my-slack type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/XXX - name: my-slack2 type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/TTT` noActions string = ` Name: tenant routes: - name: route1 actions: ["my-slack3"] template: raw plugins: Policy-Show-All: true templates: - name: raw body: | package postee result:=input` noTemplates string = ` Name: tenant routes: - name: route1 actions: ["my-slack", "my-slack2"] template: raw plugins: Policy-Show-All: true actions: - name: my-slack type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/XXX - name: my-slack2 type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/TTT` invalidTemplate string = ` Name: tenant routes: - name: route1 actions: ["my-slack"] template: rawx plugins: Policy-Show-All: true templates: - name: raw body: | package postee result:=input actions: - name: my-slack type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/TTT` invalidAction string = ` Name: tenant routes: - name: route1 actions: ["x-slack"] template: raw plugins: Policy-Show-All: true templates: - name: raw body: | package postee result:=input actions: - name: my-slack type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/TTT` singleRouteSingelInput string = ` Name: tenant routes: - name: fail_evaluation actions: ["my-slack"] template: raw input-files: - Allow-Registry.rego templates: - name: raw body: | package postee result:=input actions: - name: my-slack type: slack enable: true url: https://hooks.slack.com/services/ABCDF/1234/TTT` payload = `{"image" : "alpine"}` ) func TestHandling(t *testing.T) { tests := []struct { caseDesc string cfg string expctdInvctns []invctn }{ { "Single Route", singleRoute, []invctn{ { "*actions.SlackAction", "*regoservice.regoEvaluator", "route1", }, }, }, { "2 Routes", twoRoutes, []invctn{ { "*actions.SlackAction", "*regoservice.regoEvaluator", "route1", }, { "*actions.SlackAction", "*regoservice.regoEvaluator", "route2", }, }, }, { "2 Actions per single route", twoActions, []invctn{ { "*actions.SlackAction", "*regoservice.regoEvaluator", "route1", }, { "*actions.SlackAction", "*regoservice.regoEvaluator", "route1", }, }, }, { "No Actions configured", noActions, []invctn{}, }, { "No Template configured", noTemplates, []invctn{}, }, { "Invalid Action reference", invalidAction, []invctn{}, }, { "Invalid Template reference", invalidTemplate, []invctn{}, }, { "No actions associated with route", noAssociatedAction, []invctn{}, }, } for _, test := range tests { runTestRouteHandlingCase(t, test.caseDesc, test.cfg, test.expctdInvctns) } } func runTestRouteHandlingCase(t *testing.T, caseDesc string, cfg string, expctdInvctns []invctn) { actualInvctCnt := 0 t.Logf("Case: %s\n", caseDesc) wrap := ctxWrapper{} wrap.setup(cfg) defer wrap.teardown() err := wrap.instance.Start(wrap.cfgPath) if err != nil { t.Fatalf("[%s] Unexpected error %v", caseDesc, err) } wrap.instance.handle([]byte(payload)) timeoutDuration := 3 * time.Second if len(expctdInvctns) == 0 { timeoutDuration = time.Second } timeout := time.After(timeoutDuration) for { select { case <-timeout: if len(expctdInvctns) > 0 { t.Fatal("test didn't finish in time") } return case r := <-wrap.buff: t.Logf("[%s] received invocation (%s, %s, %s)", caseDesc, r.routeName, r.actionCls, r.templateCls) actualInvctCnt++ found := false for _, expect := range expctdInvctns { if r == expect { found = true break } } if actualInvctCnt == len(expctdInvctns) { return //everything is ok, exiting } if !found && len(expctdInvctns) > 0 { t.Errorf("[%s] Unexpected invocation (%s, %s, %s)", caseDesc, r.routeName, r.actionCls, r.templateCls) return } if actualInvctCnt > len(expctdInvctns) { t.Errorf("[%s] Service should be called %d times but called %d times", caseDesc, len(expctdInvctns), actualInvctCnt) return } } } } func TestInvalidRouteName(t *testing.T) { expctdInvctns := 0 actualInvctCnt := 0 wrap := ctxWrapper{} wrap.setup(singleRoute) defer wrap.teardown() err := wrap.instance.Start(wrap.cfgPath) if err != nil { t.Fatalf("Unexpected error %v", err) } wrap.instance.HandleRoute("not-exist", []byte(payload)) timeout := time.After(1 * time.Second) for { select { case <-timeout: return case <-wrap.buff: actualInvctCnt++ if actualInvctCnt > expctdInvctns { t.Errorf("Service shouldn't be called if invalid route is specified") return } } } } func TestRouteWithNoValidRego(t *testing.T) { expctdInvctns := 0 actualInvctCnt := 0 wrap := ctxWrapper{} wrap.setup(singleRouteSingelInput) defer wrap.teardown() err := wrap.instance.Start(wrap.cfgPath) if err != nil { t.Fatalf("Unexpected error %v", err) } wrap.instance.HandleRoute("fail_evaluation", []byte(payload)) timeout := time.After(1 * time.Second) for { select { case <-timeout: return case <-wrap.buff: actualInvctCnt++ if actualInvctCnt > expctdInvctns { t.Errorf("Service shouldn't be called if invalid route is specified") return } } } } func TestSend(t *testing.T) { expctdInvctns := 1 actualInvctCnt := 0 wrap := ctxWrapper{} wrap.setup(singleRoute) defer wrap.teardown() err := wrap.instance.Start(wrap.cfgPath) if err != nil { t.Fatalf("Unexpected error %v", err) } wrap.instance.Send([]byte(payload)) timeout := time.After(1 * time.Second) for { select { case <-timeout: return case <-wrap.buff: actualInvctCnt++ if actualInvctCnt != expctdInvctns { t.Errorf("Service shouldn't be called once") return } } } } ================================================ FILE: router/router.go ================================================ package router import ( "bytes" "container/ring" "errors" "fmt" "io/ioutil" "log" "net/http" "path" "strings" "sync" "time" "github.com/aquasecurity/postee/v2/actions" "github.com/aquasecurity/postee/v2/data" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/formatting" "github.com/aquasecurity/postee/v2/msgservice" "github.com/aquasecurity/postee/v2/regoservice" "github.com/aquasecurity/postee/v2/routes" "github.com/aquasecurity/postee/v2/utils" "github.com/ghodss/yaml" "github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats.go" ) const ( ServiceNowTableDefault = "incident" AnonymizeReplacement = "" ) type Router struct { ConfigCh chan *nats.Msg Mode string NatsServer *server.Server NatsConn *nats.Conn NatsMsgCh chan *nats.Msg RunnerName string ControllerURL string mutexScan sync.Mutex quit chan struct{} readOnlyEvents *ring.Ring inputEventQueue chan []byte ticker *time.Ticker stopTicker chan struct{} cfgfile string aquaServer string actions map[string]actions.Action inputRoutes map[string]*routes.InputRoute templates map[string]data.Inpteval } var ( initCtx sync.Once routerCtx *Router baseForTicker = time.Hour requireAuthorization = map[string]bool{ "servicenow": true, } ) func Instance() *Router { initCtx.Do(func() { routerCtx = &Router{ mutexScan: sync.Mutex{}, quit: make(chan struct{}), readOnlyEvents: ring.New(1000), inputEventQueue: make(chan []byte, 1000), actions: make(map[string]actions.Action), inputRoutes: make(map[string]*routes.InputRoute), templates: make(map[string]data.Inpteval), stopTicker: make(chan struct{}), } }) return routerCtx } func (ctx *Router) ReloadConfig() { ctx.Terminate() err := ctx.Start(ctx.cfgfile) if err != nil { log.Printf("Unable to start router: %s", err) } } func (ctx *Router) Start(cfgfile string) error { log.Printf("Starting Router....") ctx.cfgfile = cfgfile ctx.actions = map[string]actions.Action{} ctx.inputRoutes = map[string]*routes.InputRoute{} ctx.templates = map[string]data.Inpteval{} ctx.ticker = nil err := ctx.load() if err != nil { return err } go ctx.listen() return nil } func (ctx *Router) Terminate() { log.Printf("Terminating Router....") if ctx.NatsConn != nil { log.Println("Closing NATS connection") ctx.NatsConn.Close() log.Println("NATS termination complete") } for _, pl := range ctx.actions { err := pl.Terminate() if err != nil { log.Printf("failed to terminate action: %v", err) } } log.Printf("Actions terminated") for _, route := range ctx.inputRoutes { route.StopScheduler() } log.Printf("Route schedulers stopped") ctx.quit <- struct{}{} log.Printf("quit notified") if ctx.ticker != nil { ctx.stopTicker <- struct{}{} log.Printf("stopTicker notified") } } func (ctx *Router) Send(data []byte) { ctx.inputEventQueue <- data ctx.readOnlyEvents.Value = data ctx.readOnlyEvents = ctx.readOnlyEvents.Next() } func (ctx *Router) GetCurrentEvents() []any { var events []any ctx.readOnlyEvents.Do(func(a any) { events = append(events, a) }) return events } func (ctx *Router) initTemplate(template *Template) error { log.Printf("Configuring template %s \n", template.Name) if template.LegacyScanRenderer != "" { inpteval, err := formatting.BuildLegacyScnEvaluator(template.LegacyScanRenderer) if err != nil { return err } ctx.templates[template.Name] = inpteval log.Printf("Configured with legacy renderer %s \n", template.LegacyScanRenderer) } if template.RegoPackage != "" { inpteval, err := regoservice.BuildBundledRegoEvaluator(template.RegoPackage) if err != nil { return err } ctx.templates[template.Name] = inpteval log.Printf("Configured with Rego package %s\n", template.RegoPackage) } if template.Url != "" { log.Printf("Configured with url: %s\n", template.Url) r, err := http.NewRequest("GET", template.Url, nil) if err != nil { return err } httpClient := getHttpClient() resp, err := httpClient.Do(r) if err != nil { return err } if resp.StatusCode > 399 { return errors.New(fmt.Sprintf("can not connect to %s, response status is %d", template.Url, resp.StatusCode)) } b, err := ioutil.ReadAll(resp.Body) if err != nil { return err } defer resp.Body.Close() inpteval, err := regoservice.BuildExternalRegoEvaluator(path.Base(r.URL.Path), string(b)) if err != nil { return err } ctx.templates[template.Name] = inpteval } //body goes last to provide an option to keep body in config but not use it if template.Body != "" { inpteval, err := regoservice.BuildExternalRegoEvaluator("inline.rego", template.Body) if err != nil { return err } ctx.templates[template.Name] = inpteval } return nil } func (ctx *Router) load() error { ctx.mutexScan.Lock() defer ctx.mutexScan.Unlock() log.Printf("Loading alerts configuration file %s ....\n", ctx.cfgfile) tenant, err := Parsev2cfg(ctx.cfgfile) if err != nil { return err } if len(tenant.AquaServer) > 0 { var slash string if !strings.HasSuffix(tenant.AquaServer, "/") { slash = "/" } ctx.aquaServer = fmt.Sprintf("%s%s#/images/", tenant.AquaServer, slash) } dbservice.DbSizeLimit = parseSize(tenant.DBMaxSize) if tenant.DBTestInterval == 0 { tenant.DBTestInterval = 1 } ctx.ticker = time.NewTicker(baseForTicker * time.Duration(tenant.DBTestInterval)) go func() { for { select { case <-ctx.stopTicker: return case <-ctx.ticker.C: dbservice.CheckSizeLimit() dbservice.CheckExpiredData() } } }() for i, r := range tenant.InputRoutes { ctx.inputRoutes[r.Name] = routes.ConfigureTimeouts(&tenant.InputRoutes[i]) } for _, t := range tenant.Templates { err := ctx.initTemplate(&t) if err != nil { log.Printf("Can not initialize template %s: %v \n", t.Name, err) } } for _, settings := range tenant.Actions { utils.Debug("%#v\n", anonymizeSettings(&settings)) if settings.Enable { plg := BuildAndInitOtpt(&settings, ctx.aquaServer) if plg != nil { log.Printf("Action %s is configured", settings.Name) ctx.actions[settings.Name] = plg } } } return nil } type service interface { MsgHandling(input []byte, output actions.Action, route *routes.InputRoute, inpteval data.Inpteval, aquaServer *string) EvaluateRegoRule(input *routes.InputRoute, in []byte) bool } var getScanService = func() service { serv := &msgservice.MsgService{} return serv } var getHttpClient = func() *http.Client { return http.DefaultClient } func (ctx *Router) HandleRoute(routeName string, in []byte) { r, ok := ctx.inputRoutes[routeName] if !ok || r == nil { log.Printf("No route found: %q", routeName) return } if len(r.Actions) == 0 { log.Printf("Route %q has no actions", routeName) return } // send event up to controller unconditionally, in case controller knows if ctx.Mode == "runner" { log.Println("Sending event upstream to controller at url: ", ctx.ControllerURL) NATSEventSubject := "postee.events" if err := ctx.NatsConn.Publish(NATSEventSubject, in); err != nil { // TODO: What happens if controller is unavailable? log.Println("Unable to send event upstream to controller at url: ", ctx.ControllerURL, "err: ", err.Error()) } } if !getScanService().EvaluateRegoRule(r, in) { return } for _, ra := range r.Actions { handle := true if ctx.Mode == "controller" { controller, err := Parsev2cfg(ctx.cfgfile) if err != nil { log.Println("Unable to parse cfgfile for controller: ", err) return } for _, ca := range controller.Actions { if ra == ca.Name { if ca.RunsOn != "" { log.Println("Skipping: ", ca.Name, "as it is for runner: ", ca.RunsOn) handle = false break // skip as it is for runner to run } } } } if !handle { continue } pl, ok := ctx.actions[ra] if !ok { log.Printf("route %q contains an action %q, which isn't enabled now.", routeName, ra) continue } tmpl, ok := ctx.templates[r.Template] if !ok { log.Printf("route %q contains reference to undefined or misconfigured template %q.", routeName, r.Template) continue } log.Printf("route %q is associated with template %q", routeName, r.Template) if r.SerializeActions { getScanService().MsgHandling(in, pl, r, tmpl, &ctx.aquaServer) } else { go getScanService().MsgHandling(in, pl, r, tmpl, &ctx.aquaServer) } } } func (ctx *Router) handle(in []byte) { for routeName := range ctx.inputRoutes { ctx.HandleRoute(routeName, in) } } func BuildAndInitOtpt(settings *ActionSettings, aquaServerUrl string) actions.Action { settings.User = utils.GetEnvironmentVarOrPlain(settings.User) if len(settings.User) == 0 && requireAuthorization[settings.Type] { log.Printf("User for %q is empty", settings.Name) return nil } settings.Password = utils.GetEnvironmentVarOrPlain(settings.Password) if len(settings.Password) == 0 && requireAuthorization[settings.Type] { log.Printf("Password for %q is empty", settings.Name) return nil } settings.Token = utils.GetEnvironmentVarOrPlain(settings.Token) if settings.Type == "jira" { if len(settings.User) == 0 { log.Printf("User for %q is empty", settings.Name) return nil } if len(settings.Token) == 0 && len(settings.Password) == 0 { log.Printf("Password and Token for %q are empty", settings.Name) return nil } } utils.Debug("Starting Action %q: %q\n", settings.Type, settings.Name) var plg actions.Action var err error switch strings.ToLower(settings.Type) { case "jira": plg = buildJiraAction(settings) case "email": plg = buildEmailAction(settings) case "slack": plg = buildSlackAction(settings, aquaServerUrl) case "teams": plg = buildTeamsAction(settings, aquaServerUrl) case "servicenow": plg = buildServiceNow(settings) case "webhook": plg = buildWebhookAction(settings) case "splunk": plg = buildSplunkAction(settings) case "stdout": plg = buildStdoutAction(settings) case "nexusiq": plg = buildNexusIqAction(settings) case "dependencytrack": plg = buildDependencyTrackAction(settings) case "opsgenie": plg = buildOpsGenieAction(settings) case "exec": plg, err = buildExecAction(settings) if err != nil { log.Println(err.Error()) return nil } case "http": plg, err = buildHTTPAction(settings) if err != nil { log.Println(err.Error()) return nil } case "kubernetes": plg, err = buildKubernetesAction(settings) if err != nil { log.Println(err.Error()) return nil } case "docker": plg, err = buildDockerAction(settings) if err != nil { log.Println(err.Error()) return nil } case "awssecurityhub": plg, err = buildAWSSecurityHubAction(settings) if err != nil { log.Println(err.Error()) return nil } case "pagerduty": plg, err = buildPagerdutyAction(settings) if err != nil { log.Println(err.Error()) return nil } default: log.Printf("Action type %q is undefined or empty. Action name is %q.", settings.Type, settings.Name) return nil } err = plg.Init() if err != nil { log.Printf("failed to Init : %v", err) return nil } return plg } func (ctx *Router) listen() { for { select { case <-ctx.quit: return case data := <-ctx.inputEventQueue: go ctx.handle(bytes.ReplaceAll(data, []byte{'`'}, []byte{'\''})) case msg := <-ctx.ConfigCh: log.Println("A runner requested config: ", string(msg.Data)) cfg, err := buildRunnerConfig(string(msg.Data), ctx.cfgfile) if err != nil { log.Println("Failed to build config to send to runner: ", string(msg.Data), "err: ", err) } if err = msg.Respond([]byte(cfg)); err != nil { log.Println("Failed to send config to runner: ", err) } case msg := <-ctx.NatsMsgCh: // TODO: Add logging to capture all received events log.Println("Received incoming event from runner: ", string(msg.Data)) go ctx.handle(bytes.ReplaceAll(msg.Data, []byte{'`'}, []byte{'\''})) } } } // TODO: Improve parsing logic func buildRunnerConfig(runnerName, cfgFile string) (string, error) { tenant, err := Parsev2cfg(cfgFile) if err != nil { return "", err } var runnerRoutes []routes.InputRoute var runnerActions []ActionSettings var runnerTemplates []Template for _, output := range tenant.Actions { if output.RunsOn == runnerName { runnerActions = append(runnerActions, output) } } for _, ro := range runnerActions { for _, inputRoute := range tenant.InputRoutes { for _, inputAction := range inputRoute.Actions { if ro.Name == inputAction { runnerRoute := inputRoute var oNames []string for _, o := range runnerActions { oNames = append(oNames, o.Name) } runnerRoute.Actions = oNames runnerRoutes = append(runnerRoutes, runnerRoute) } } } } for _, rr := range runnerRoutes { for _, inputTemplate := range tenant.Templates { if inputTemplate.Name == rr.Template && !contains(runnerTemplates, inputTemplate.Name) { runnerTemplates = append(runnerTemplates, inputTemplate) } } } tenant.InputRoutes = runnerRoutes tenant.Actions = runnerActions tenant.Templates = runnerTemplates cfgB, err := yaml.Marshal(tenant) if err != nil { return "", err } return string(cfgB), nil } func contains(haystack []Template, needle string) bool { for _, noodle := range haystack { if noodle.Name == needle { return true } } return false } func SetupConnOptions(opts []nats.Option) []nats.Option { totalWait := 10 * time.Minute reconnectDelay := 2 * time.Second opts = append(opts, nats.ReconnectWait(reconnectDelay)) opts = append(opts, nats.MaxReconnects(int(totalWait/reconnectDelay))) opts = append(opts, nats.DisconnectErrHandler(func(nc *nats.Conn, err error) { log.Printf("Disconnected due to: %s, will attempt reconnects for %.0fm", err, totalWait.Minutes()) })) opts = append(opts, nats.ReconnectHandler(func(nc *nats.Conn) { log.Printf("Reconnected [%s]", nc.ConnectedUrl()) })) return opts } ================================================ FILE: router/router_test.go ================================================ package router import ( "testing" "github.com/stretchr/testify/assert" ) func Test_buildRunnerConfig(t *testing.T) { testCases := []struct { name string cfgFile string want string expectedError string }{ { name: "happy path", cfgFile: "goldens/sample.cfg", want: `actions: - enable: true env: - MY_ENV_VAR=foo_bar_baz - MY_KEY=secret exec-script: | #!/bin/sh echo $POSTEE_EVENT echo "this is hello from postee" name: my-exec-from-runner runs-on: postee-runner-1 type: exec - body-content: | This is an another example of a inline body Event ID: event.input.SigMetadata.ID enable: true method: POST name: my-http-post-from-runner runs-on: postee-runner-1 type: http url: https://webhook.site/ db-verify-interval: 1 max-db-size: 1000MB routes: - actions: - my-exec-from-runner - my-http-post-from-runner input: contains(input.SigMetadata.ID, "TRC-1") name: runner-only-route plugins: {} serialize-actions: true template: raw-json - actions: - my-exec-from-runner - my-http-post-from-runner input: contains(input.SigMetadata.ID, "TRC-2") name: controller-runner-route plugins: {} serialize-actions: true template: raw-json - actions: - my-exec-from-runner - my-http-post-from-runner input: contains(input.SigMetadata.ID, "TRC-1") name: runner-only-route plugins: {} serialize-actions: true template: raw-json - actions: - my-exec-from-runner - my-http-post-from-runner input: contains(input.SigMetadata.ID, "TRC-2") name: controller-runner-route plugins: {} serialize-actions: true template: raw-json templates: - name: raw-json rego-package: postee.rawmessage.json`, }, { name: "sad path, config not found", cfgFile: "invalid path", expectedError: "open invalid path: no such file or directory", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got, err := buildRunnerConfig("postee-runner-1", tc.cfgFile) switch { case tc.expectedError != "": assert.Equal(t, tc.expectedError, err.Error(), tc.name) assert.Empty(t, got, tc.name) default: assert.NoError(t, err, tc.name) assert.YAMLEq(t, tc.want, got, tc.name) } }) } } ================================================ FILE: router/rule.go ================================================ package router type Rule struct { Name string `json:"name,omitempty"` ID string `json:"id,omitempty"` } ================================================ FILE: router/sizeparser.go ================================================ package router import ( "log" "regexp" "strconv" "strings" ) const ( B = 1 KB = 1024 MB = 1024 * KB GB = 1024 * MB ) var ( sizeRegex = regexp.MustCompile(`^(\d+) ?([kKmMgG]?[bB]?)$`) suffixes = map[string]int{"b": B, "kb": KB, "mb": MB, "gb": GB} parseError = "unable parse MaxDBSize, unlimited size used" ) func parseSize(sizeStr string) int { if sizeStr == "" { return 0 } matches := sizeRegex.FindStringSubmatch(sizeStr) if matches != nil { size, err := strconv.Atoi(matches[1]) if err != nil { log.Println(parseError) return 0 } if matches[2] != "" { suffix := suffixes[strings.ToLower(matches[2])] return size * suffix } else { return size } } else { log.Println(parseError) return 0 } } ================================================ FILE: router/sizeparser_test.go ================================================ package router import ( "fmt" "github.com/stretchr/testify/assert" "math" "testing" ) func TestParseSize(t *testing.T) { tests := []struct { name string sizeString string wantSize int }{ { name: "happy path(empty string is used)", sizeString: "", wantSize: 0, }, { name: "happy path(suffix 'b' is used)", sizeString: "1b", wantSize: 1, }, { name: "happy path(suffix 'kb' is used)", sizeString: "2kb", wantSize: 2 * KB, }, { name: "happy path(suffix 'Mb' is used)", sizeString: "3Mb", wantSize: 3 * MB, }, { name: "happy path(suffix 'GB' is used)", sizeString: "4GB", wantSize: 4 * GB, }, { name: "happy path(suffix ' b' is used)", sizeString: "5 b", wantSize: 5, }, { name: "happy path(suffix is not used)", sizeString: "6", wantSize: 6, }, { name: "sad path(suffix 'tb' is used)", sizeString: "7TB", wantSize: 0, }, { name: "sad path(float value is used)", sizeString: "8.8", wantSize: 0, }, { name: "sad path(value more than MaxInt)", sizeString: fmt.Sprintf("%d1", math.MaxInt), wantSize: 0, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { size := parseSize(test.sizeString) assert.EqualValues(t, test.wantSize, size) }) } } ================================================ FILE: router/template.go ================================================ package router type Template struct { Name string `json:"name,omitempty"` Body string `json:"body,omitempty"` RegoPackage string `json:"rego-package,omitempty"` LegacyScanRenderer string `json:"legacy-scan-renderer,omitempty"` Url string `json:"url,omitempty"` } ================================================ FILE: router/tenants.go ================================================ package router import ( "github.com/aquasecurity/postee/v2/routes" ) type TenantSettings struct { AquaServer string `json:"aqua-server,omitempty"` DBMaxSize string `json:"max-db-size,omitempty"` DBRemoveOldData int `json:"delete-old-data,omitempty"` DBTestInterval int `json:"db-verify-interval,omitempty"` Actions []ActionSettings `json:"actions,omitempty"` InputRoutes []routes.InputRoute `json:"routes,omitempty"` Templates []Template `json:"templates,omitempty"` Rules []Rule `json:"rules,omitempty"` } ================================================ FILE: routes/aggrtimeout.go ================================================ package routes import ( "log" "strconv" "strings" ) func parseTimeouts(v string) (int, error) { var timeout int var err error times := map[string]int{ "s": 1, "m": 60, "h": 3600, "d": 86400, } v = strings.ReplaceAll(v, " ", "") if v == "" { return 0, nil } wasConvert := false for suffix, k := range times { if strings.HasSuffix(strings.ToLower(v), suffix) { timeout, err = strconv.Atoi(strings.TrimSuffix(v, suffix)) timeout *= k wasConvert = true break } } if !wasConvert { timeout, err = strconv.Atoi(v) } return timeout, err } func ConfigureTimeouts(route *InputRoute) *InputRoute { aggregateTimeoutSeconds, err := parseTimeouts(route.Plugins.AggregateMessageTimeout) if err != nil { log.Printf("%q settings: Can't convert 'aggregate-message-timeout'(%q) to seconds.", route.Name, route.Plugins.AggregateMessageTimeout) } route.Plugins.AggregateTimeoutSeconds = aggregateTimeoutSeconds uniqueMessageTimeoutSeconds, err := parseTimeouts(route.Plugins.UniqueMessageTimeout) if err != nil { log.Printf("%q settings: Can't convert 'unique-message-timeout'(%q) to seconds.", route.Name, route.Plugins.UniqueMessageTimeout) } route.Plugins.UniqueMessageTimeoutSeconds = uniqueMessageTimeoutSeconds return route } ================================================ FILE: routes/aggrtimeout_test.go ================================================ package routes import "testing" var ( tests = []struct { caseDesc string timeout string expctdSeconds int }{ { "One minute", "1m", 60, }, { "Six hundredths seconds", "600s", 600, }, { "Two hours", "2h", 7200, }, { "Two days", "2d", 172800, }, { "Two days with space between", "2 d", 172800, }, { "Exact number of seconds", "300", 300, }, { "Invalid format", "xxxl", 0, }, { "Empty string", "", 0, }, { "a space", " ", 0, }, } ) func TestTimeouts(t *testing.T) { for _, test := range tests { route := &InputRoute{} route.Plugins.AggregateMessageTimeout = test.timeout route.Plugins.UniqueMessageTimeout = test.timeout route = ConfigureTimeouts(route) if route.Plugins.AggregateTimeoutSeconds != test.expctdSeconds { t.Errorf("[%s] Invalid number of seconds in AggregateTimeoutSeconds, expected %d, got %d \n", test.caseDesc, test.expctdSeconds, route.Plugins.AggregateTimeoutSeconds) } if route.Plugins.UniqueMessageTimeoutSeconds != test.expctdSeconds { t.Errorf("[%s] Invalid number of seconds in UniqueMessageTimeout, expected %d, got %d \n", test.caseDesc, test.expctdSeconds, route.Plugins.UniqueMessageTimeoutSeconds) } } } ================================================ FILE: routes/routes.go ================================================ package routes type InputRoute struct { Name string `json:"name,omitempty"` Input string `json:"input,omitempty"` InputFiles []string `json:"input-files,omitempty"` Actions []string `json:"actions,omitempty"` Plugins Plugins `json:"plugins,omitempty"` Template string `json:"template,omitempty"` SerializeActions bool `json:"serialize-actions,omitempty"` Scheduling chan struct{} `json:"-"` } type Plugins struct { AggregateMessageNumber int `json:"aggregate-message-number,omitempty"` AggregateMessageTimeout string `json:"aggregate-message-timeout,omitempty"` AggregateTimeoutSeconds int `json:"aggregate-timeout-seconds,omitempty"` UniqueMessageProps []string `json:"unique-message-props,omitempty"` UniqueMessageTimeout string `json:"unique-message-timeout,omitempty"` UniqueMessageTimeoutSeconds int `json:"unique-message-timeout-seconds,omitempty"` } func (route *InputRoute) IsSchedulerRun() bool { return route.Scheduling != nil } func (route *InputRoute) StartScheduler() { route.Scheduling = make(chan struct{}) } func (route *InputRoute) StopScheduler() { if route.Scheduling != nil { close(route.Scheduling) } } ================================================ FILE: routes/routes_test.go ================================================ package routes import ( "testing" ) func TestScheduling(t *testing.T) { stopCh := make(chan struct{}) demoRoute1 := &InputRoute{} demoRoute1Stopped := false demoRoute1.StartScheduler() if !demoRoute1.IsSchedulerRun() { t.Errorf("Route 1 is not started") } go func() { <-demoRoute1.Scheduling demoRoute1Stopped = true stopCh <- struct{}{} }() demoRoute1.StopScheduler() <-stopCh if !demoRoute1Stopped { t.Errorf("Route 1 is not stopped") } demoRoute2 := &InputRoute{} if demoRoute2.IsSchedulerRun() { t.Errorf("Route 2 should not be started") } demoRoute2.StopScheduler() } ================================================ FILE: runner/runner.go ================================================ package runner import ( "fmt" "log" "os" "time" "github.com/aquasecurity/postee/v2/router" "github.com/nats-io/nats.go" ) const ( NATSConfigSubject = "postee.config" ) type Runner struct { ControllerURL string RunnerSeedFilePath string RunnerCARootPath string RunnerTLSKeyPath string RunnerTLSCertPath string RunnerName string } func (r Runner) Setup(rtr *router.Router, cfg *os.File) error { log.Println("Running in runner mode") if r.ControllerURL == "" { return fmt.Errorf("runner mode requires a valid controller url") } var opts []nats.Option var nKeyOpt nats.Option if r.RunnerSeedFilePath != "" { log.Println("Seedfile specified for Runner, enabling AuthN") var err error nKeyOpt, err = nats.NkeyOptionFromSeed(r.RunnerSeedFilePath) if err != nil { return fmt.Errorf("unable to parse seed file: %w", err) } opts = append(opts, nKeyOpt) } if r.RunnerTLSKeyPath != "" && r.RunnerTLSCertPath != "" { opts = append(opts, nats.ClientCert(r.RunnerTLSCertPath, r.RunnerTLSKeyPath)) if r.RunnerCARootPath != "" { opts = append(opts, nats.RootCAs(r.RunnerCARootPath)) } } var err error rtr.NatsConn, err = nats.Connect(r.ControllerURL, router.SetupConnOptions(opts)...) if err != nil { return fmt.Errorf("unable to connect to controller at url: %s, err: %w", r.ControllerURL, err) } msg, err := rtr.NatsConn.Request(NATSConfigSubject, []byte(r.RunnerName), time.Second*5) if err != nil { return fmt.Errorf("unable to obtain runner config from url: %s, err: %w", r.ControllerURL, err) } if _, err = cfg.Write(msg.Data); err != nil { return fmt.Errorf("unable to write runner config to disk: %w", err) } log.Println("Runner configuration obtained from: ", r.ControllerURL) rtr.ControllerURL = r.ControllerURL rtr.RunnerName = r.RunnerName rtr.Mode = "runner" return nil } ================================================ FILE: servicenow/insert_table.go ================================================ package servicenow_api import ( "bytes" "encoding/base64" "fmt" "net/http" "github.com/aquasecurity/postee/v2/utils" ) func InsertRecordToTable(user, password, instance, table string, content []byte) error { url := fmt.Sprintf("https://%s.%s%s%s%s", instance, BaseServer, baseApiUrl, tableApi, table) r := bytes.NewReader(content) client := http.DefaultClient reg, err := http.NewRequest("POST", url, r) if err != nil { return err } reg.Header.Add("Content-Type", "application/json") reg.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(user+":"+password))) resp, err := client.Do(reg) if err != nil { return err } if resp.StatusCode != http.StatusCreated { return fmt.Errorf("InsertRecordToTable Error: %v\nHeader: %v", resp.Status, utils.PrnLogResponse(resp.Body)) } return nil } ================================================ FILE: servicenow/servicenow_base.go ================================================ package servicenow_api const ( BaseServer = "service-now.com/" baseApiUrl = "api/now/" tableApi = "table/" ) type ServiceNowData struct { ShortDescription string `json:"short_description"` WorkNotes string `json:"work_notes"` Opened string `json:"opened_at"` Caller string `json:"caller_id"` Category string `json:"category"` Subcategory string `json:"subcategory"` Impact int `json:"impact"` Urgency int `json:"urgency"` State int `json:"state"` Description string `json:"description"` AssignedTo string `json:"assigned_to"` AssignmentGroup string `json:"assignment_group"` } ================================================ FILE: slack/sendtoslack.go ================================================ package slack_api import ( "bytes" "fmt" "io/ioutil" "log" "net/http" ) func SendToUrl(url string, data []byte) error { r := bytes.NewReader(data) resp, err := http.Post(url, "application/json", r) if err != nil { log.Printf("Slack API error: %v", err) return err } if resp.StatusCode != http.StatusOK { defer resp.Body.Close() msg, err := ioutil.ReadAll(resp.Body) if err != nil { return err } return fmt.Errorf("Slack API error: Status: %q. Message: %q", resp.Status, msg) } return nil } ================================================ FILE: teams/teams_requests.go ================================================ package teams_api import ( "bytes" "fmt" "io/ioutil" "net/http" "github.com/aquasecurity/postee/v2/utils" ) func CreateMessageByWebhook(webhook, content string) error { var message bytes.Buffer fmt.Fprintf(&message, "{\"text\":\"%s\"}", content) utils.Debug("Data for sending to %q: %q\n", webhook, message.String()) r := bytes.NewReader(message.Bytes()) client := http.DefaultClient reg, err := http.NewRequest("POST", webhook, r) if err != nil { return err } reg.Header.Add("Content-Type", "application/json") resp, err := client.Do(reg) if err != nil { return err } defer resp.Body.Close() if message, _ := ioutil.ReadAll(resp.Body); resp.StatusCode != http.StatusOK { return fmt.Errorf("InsertRecordToTable Error: %q. %s", resp.Status, message) } else { if message[0] != '1' { return fmt.Errorf("Teams Body Error: %q", string(message)) } utils.Debug("Response body: %q\n", message) } return nil } ================================================ FILE: ui/backend/dbservice/getplgnstats.go ================================================ package dbservice import ( "os" "strconv" hookDbService "github.com/aquasecurity/postee/v2/dbservice" bolt "go.etcd.io/bbolt" ) func GetPlgnStats() (r map[string]int, err error) { r = make(map[string]int) var DbPath string if len(os.Getenv("PATH_TO_DB")) > 0 { DbPath = os.Getenv("PATH_TO_DB") } else { DbPath = hookDbService.DbPath } db, err := bolt.Open(DbPath, 0444, nil) if err != nil { return nil, err } defer db.Close() err = db.View(func(tx *bolt.Tx) error { bucket := tx.Bucket([]byte(hookDbService.DbBucketActionStats)) if bucket == nil { return nil //no bucket - empty stats will be returned } c := bucket.Cursor() for k, v := c.First(); k != nil; k, v = c.Next() { cnt, err := strconv.Atoi(string(v[:])) if err != nil { return err } r[string(k[:])] = cnt } return nil }) if err != nil { return nil, err } return r, nil } ================================================ FILE: ui/backend/go.mod ================================================ module github.com/aquasecurity/postee/ui/backend go 1.18 require ( github.com/aquasecurity/postee/v2 v2.6.0 github.com/gorilla/mux v1.8.0 github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.1 github.com/stretchr/testify v1.8.0 go.etcd.io/bbolt v1.3.6 ) require ( github.com/Microsoft/go-winio v0.5.1 // indirect github.com/OneOfOne/xxhash v1.2.8 // indirect github.com/agnivade/levenshtein v1.1.1 // indirect github.com/aquasecurity/go-jira v0.0.0-20211103111421-b62ce48827be // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker v20.10.24+incompatible // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.4.0 // indirect github.com/fatih/structs v1.1.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-logr/logr v1.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.2.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/google/go-cmp v0.5.8 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.2.0 // indirect github.com/googleapis/gnostic v0.5.5 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.5.3 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.14.4 // indirect github.com/minio/highwayhash v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 // indirect github.com/nats-io/nats-server/v2 v2.7.4 // indirect github.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d // indirect github.com/nats-io/nkeys v0.3.0 // indirect github.com/nats-io/nuid v1.0.1 // indirect github.com/open-policy-agent/opa v0.44.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/tidwall/gjson v1.14.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/trivago/tgo v1.0.7 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/net v0.7.0 // indirect golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/term v0.5.0 // indirect golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/api v0.23.3 // indirect k8s.io/apimachinery v0.23.3 // indirect k8s.io/client-go v0.23.3 // indirect k8s.io/klog/v2 v2.30.0 // indirect k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) replace ( github.com/containerd/containerd v1.6.2 => github.com/containerd/containerd v1.6.6 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 => golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f ) ================================================ FILE: ui/backend/go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Microsoft/go-winio v0.5.1 h1:aPJp2QD7OOrhO5tQXqQoGSJc+DjDtWTGLOmNyAm6FgY= github.com/Microsoft/go-winio v0.5.1/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/aquasecurity/go-jira v0.0.0-20211103111421-b62ce48827be h1:xUasZnauNAn2jY0gfVG+Ro371S31s3SfVUvcjhwIMyI= github.com/aquasecurity/go-jira v0.0.0-20211103111421-b62ce48827be/go.mod h1:IHtKzIAdk0t3Xse7rJSY7pJlA8gB7lqY2b4l5WYZYsk= github.com/aquasecurity/postee/v2 v2.6.0 h1:3UH5b7LlGLSRMMAMvjCYUwYGbkV1Wa1TQ+nlG7eoO2Y= github.com/aquasecurity/postee/v2 v2.6.0/go.mod h1:mAE5BlBooI8z3nWHldckWcCwP1smfIVwPSLtvSAujzM= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/bytecodealliance/wasmtime-go v0.36.0 h1:B6thr7RMM9xQmouBtUqm1RpkJjuLS37m6nxX+iwsQSc= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU= github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-retryablehttp v0.5.1/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.5.3 h1:QlWt0KvWT0lq8MFppF9tsJGF+ynG7ztc2KIPhzRGk7s= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.14.4 h1:eijASRJcobkVtSt81Olfh7JX43osYLwy5krOJo6YEu4= github.com/klauspost/compress v1.14.4/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY= github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= github.com/nats-io/nats-server/v2 v2.7.4 h1:c+BZJ3rGzUKCBIM4IXO8uNT2u1vajGbD1kPA6wqCEaM= github.com/nats-io/nats-server/v2 v2.7.4/go.mod h1:1vZ2Nijh8tcyNe8BDVyTviCd9NYzRbubQYiEHsvOQWc= github.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d h1:zJf4l8Kp67RIZhoVeniSLZs69SHNgjLHz0aNsqPPlx8= github.com/nats-io/nats.go v1.13.1-0.20220308171302-2f2f6968e98d/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= github.com/nats-io/nkeys v0.3.0 h1:cgM5tL53EvYRU+2YLXIK0G2mJtK12Ft9oeooSZMA2G8= github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= github.com/open-policy-agent/opa v0.44.0 h1:sEZthsrWBqIN+ShTMJ0Hcz6a3GkYsY4FaB2S/ou2hZk= github.com/open-policy-agent/opa v0.44.0/go.mod h1:YpJaFIk5pq89n/k72c1lVvfvR5uopdJft2tMg1CW/yU= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10 h1:qHnitdkr8TN/irubnQM8ml/udTyAxo6j5v61H7+TV3k= github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.10/go.mod h1:4OjcxgwdXzezqytxN534MooNmrxRD50geWZxTD7845s= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM= github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/yashtewari/glob-intersection v0.1.0 h1:6gJvMYQlTDOL3dMsPF6J0+26vwX9MB8/1q3uAdhmTrg= github.com/yashtewari/glob-intersection v0.1.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M= golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.1.0 h1:rVV8Tcg/8jHUkPUorwjaMTtemIMVXfIPKiOqnhEhakk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= k8s.io/api v0.23.3 h1:KNrME8KHGr12Ozjf8ytOewKzZh6hl/hHUZeHddT3a38= k8s.io/api v0.23.3/go.mod h1:w258XdGyvCmnBj/vGzQMj6kzdufJZVUwEM1U2fRJwSQ= k8s.io/apimachinery v0.23.3 h1:7IW6jxNzrXTsP0c8yXz2E5Yx/WTzVPTsHIx/2Vm0cIk= k8s.io/apimachinery v0.23.3/go.mod h1:BEuFMMBaIbcOqVIJqNZJXGFTP4W6AycEpb5+m/97hrM= k8s.io/client-go v0.23.3 h1:23QYUmCQ/W6hW78xIwm3XqZrrKZM+LWDqW2zfo+szJs= k8s.io/client-go v0.23.3/go.mod h1:47oMd+YvAOqZM7pcQ6neJtBiFH7alOyfunYN48VsmwE= k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 h1:E3J9oCLlaobFUqsjG9DfKbP2BmgwBL2p7pn0A3dG9W4= k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed h1:ck1fRPWPJWsMd8ZRFsWc6mh/zHp5fZ/shhbrgPUxDAE= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 h1:fD1pz4yfdADVNfFmcP2aBEtudwUQ1AlLnRBALr33v3s= sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6/go.mod h1:p4QtZmO4uMYipTQNzagwnNoseA6OxSUutVw05NhYDRs= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= sigs.k8s.io/structured-merge-diff/v4 v4.2.1 h1:bKCqE9GvQ5tiVHn5rfn1r+yao3aLQEaLzkkmAkf+A6Y= sigs.k8s.io/structured-merge-diff/v4 v4.2.1/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= ================================================ FILE: ui/backend/main.go ================================================ package main import ( "log" "os" "github.com/aquasecurity/postee/ui/backend/uiserver" ) const ( ENV_FILELOG = "POSTEE_UI_LOGFILE" ENV_CFG = "POSTEE_UI_CFG" ENV_WEB = "POSTEE_UI_WEB" ENV_UPDATE_URL = "POSTEE_UI_UPDATE_URL" ENV_PORT = "POSTEE_UI_PORT" ENV_ADMIN_USER = "POSTEE_ADMIN_USER" ENV_ADMIN_PASSWORD = "POSTEE_ADMIN_PASSWORD" DEFAULT_WEB_PATH = "/uiserver/www" ) func main() { logfile := os.Getenv(ENV_FILELOG) if logfile != "" { f, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0444) if err != nil { panic(err) } defer f.Close() log.SetOutput(f) } cfg := os.Getenv(ENV_CFG) if cfg == "" { log.Fatalf("cfg file name is empty. You have to set a filename via %q environment variable.", ENV_CFG) } web := os.Getenv(ENV_WEB) if web == "" { web = DEFAULT_WEB_PATH log.Printf("The default path to web (%q) is using now.", web) } updateUrl := os.Getenv(ENV_UPDATE_URL) if updateUrl == "" { log.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) } port := os.Getenv(ENV_PORT) if port == "" { port = "8090" log.Printf("WARNING! Using a default port: %s. You can change it via %q environment variable.", port, ENV_PORT) } admusr := os.Getenv(ENV_ADMIN_USER) if admusr == "" { admusr = "admin" log.Printf("WARNING! Using a default admin user. You can change it via %q environment variable.", ENV_ADMIN_USER) } admpwd := os.Getenv(ENV_ADMIN_PASSWORD) if admpwd == "" { admpwd = "admin" log.Printf("WARNING! Using a default admin password. You can change it via %q environment variable.", ENV_ADMIN_PASSWORD) } server := uiserver.Instance(web, port, cfg, updateUrl, admusr, admpwd) server.Start() defer server.Stop() } ================================================ FILE: ui/backend/uiserver/authentication.go ================================================ package uiserver import ( "net/http" ) const ( sessioncookiename = "postee-session-cookie" ) func (srv *uiServer) login(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() session, err := srv.store.Get(r, sessioncookiename) if err != nil { session, err = srv.store.New(r, sessioncookiename) } if session.Values["user"] == nil { frmusr := r.FormValue("username") frmpwd := r.FormValue("password") if frmusr == srv.admusr && frmpwd == srv.admpwd { session.Values["user"] = frmusr err = session.Save(r, w) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } } else { http.Error(w, "Unauthorized", http.StatusUnauthorized) } } } func (srv *uiServer) logout(w http.ResponseWriter, r *http.Request) { session, err := srv.store.Get(r, sessioncookiename) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } session.Values["user"] = "" session.Options.MaxAge = -1 err = session.Save(r, w) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } ================================================ FILE: ui/backend/uiserver/authentication_middleware.go ================================================ package uiserver import ( "net/http" "strings" ) func (srv *uiServer) authenticationMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.RequestURI, "/api") { next.ServeHTTP(w, r) return } if strings.HasPrefix(r.RequestURI, "/api/login") { next.ServeHTTP(w, r) return } if user, err := srv.getUserFromRequest(r); err == nil && user != "" { next.ServeHTTP(w, r) } else { // Write an error and stop the handler chain http.Error(w, "Forbidden", http.StatusUnauthorized) } }) } func (srv *uiServer) getUserFromRequest(r *http.Request) (string, error) { session, err := srv.store.Get(r, sessioncookiename) if err != nil { return "", err } userObj := session.Values["user"] if userObj == nil { return "", nil } else { return userObj.(string), nil } } ================================================ FILE: ui/backend/uiserver/config.go ================================================ package uiserver import ( "fmt" "io" "io/ioutil" "log" "net/http" "os" hookDbService "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/router" ) func (srv *uiServer) getConfig(w http.ResponseWriter, r *http.Request) { log.Printf("configured config path %s", srv.cfgPath) _, err := router.Parsev2cfg(srv.cfgPath) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(fmt.Sprintf("Invalid config file format: %s", err.Error()))) return } d, err := ioutil.ReadFile(srv.cfgPath) if err != nil { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) return } w.Header().Set("Content-Type", "text/yaml") w.WriteHeader(http.StatusOK) w.Write(d) } func (srv *uiServer) updateConfig(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() inputYaml, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Can't read JSON string", http.StatusBadRequest) return } if err := os.Rename(srv.cfgPath, srv.cfgPath+".copy"); err != nil { log.Printf("rename file error %v", err) http.Error(w, "Can't remove data from the config file for overwrite", http.StatusBadRequest) return } f, err := os.Create(srv.cfgPath) if err != nil { log.Printf("create file error %v", err) http.Error(w, "Can't open the config file for overwrite", http.StatusBadRequest) return } defer f.Close() _, err = f.Write(inputYaml) if err != nil { log.Printf("write file error %v", err) http.Error(w, "Can't write to the config file for overwrite", http.StatusBadRequest) return } os.RemoveAll(srv.cfgPath + ".copy") apikey, err := hookDbService.GetApiKey() if err != nil { log.Printf("Can not load api key from bolt %v", err) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } err = reloadWebhookCfg(srv.webhookUrl, apikey) if err != nil { log.Printf("Unable to reach Postee backend %v", err) http.Error(w, "Unable to reach Postee backend", http.StatusBadRequest) return } } func reloadWebhookCfg(url string, key string) error { u := fmt.Sprintf("%s/reload?key=%s", url, key) resp, err := http.Get(u) if err != nil { return err } defer resp.Body.Close() return nil } ================================================ FILE: ui/backend/uiserver/events.go ================================================ package uiserver import ( "io/ioutil" "log" "net/http" "os" ) func (srv *uiServer) getEvents(w http.ResponseWriter, r *http.Request) { log.Printf("configured config path %s", srv.cfgPath) posteeUrl := os.Getenv("POSTEE_UI_UPDATE_URL") if len(posteeUrl) <= 0 { w.WriteHeader(http.StatusBadRequest) log.Println("No Postee URL configured, set POSTEE_UI_UPDATE_URL to the Postee URL") return } resp, err := http.Get(posteeUrl + "/events") if err != nil { w.WriteHeader(http.StatusInternalServerError) log.Println("Unable to reach Postee at URL: " + posteeUrl + "/events" + " err: " + err.Error()) return } currentEvents, err := ioutil.ReadAll(resp.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) log.Println("Failed to read events: " + err.Error()) return } w.Header().Set("Content-Type", "text/json") w.WriteHeader(http.StatusOK) _, _ = w.Write(currentEvents) } ================================================ FILE: ui/backend/uiserver/events_test.go ================================================ package uiserver import ( "io/ioutil" "net/http" "net/http/httptest" "os" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestUiServer_getEvents(t *testing.T) { testCases := []struct { name string tsHandlerFunc http.HandlerFunc expectedResp string expectedStatusCode int }{ { name: "happy path", tsHandlerFunc: func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`[ { "SigMetadata":{ "ID":"TRC-2", "hostname":"postee-0" } }, { "SigMetadata":{ "ID":"TRC-3", "hostname":"postee-0" } } ]`)) }, expectedResp: `[ { "SigMetadata":{ "ID":"TRC-2", "hostname":"postee-0" } }, { "SigMetadata":{ "ID":"TRC-3", "hostname":"postee-0" } } ]`, expectedStatusCode: http.StatusOK, }, { name: "sad path, no postee url set", expectedStatusCode: http.StatusBadRequest, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { if tc.tsHandlerFunc != nil { ts := httptest.NewServer(tc.tsHandlerFunc) defer ts.Close() require.NoError(t, os.Setenv("POSTEE_UI_UPDATE_URL", ts.URL)) defer func() { _ = os.Unsetenv("POSTEE_UI_UPDATE_URL") }() } w := httptest.NewRecorder() var r *http.Request srv := uiServer{} srv.getEvents(w, r) resp := w.Result() defer func() { _ = resp.Body.Close() }() got, _ := ioutil.ReadAll(resp.Body) assert.Equal(t, tc.expectedStatusCode, resp.StatusCode, tc.name) if tc.tsHandlerFunc != nil { assert.JSONEq(t, tc.expectedResp, string(got), tc.name) } else { assert.Equal(t, tc.expectedResp, string(got), tc.name) } }) } } ================================================ FILE: ui/backend/uiserver/httpserver.go ================================================ package uiserver import ( "net/http" "os" "path/filepath" ) type localWebServer struct { localPath string url string } func (web *localWebServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { path, err := filepath.Abs(r.URL.Path) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } path = filepath.Join(web.localPath, path) _, err = os.Stat(path) if os.IsNotExist(err) { http.ServeFile(w, r, filepath.Join(web.localPath, web.url)) return } else if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } http.FileServer(http.Dir(web.localPath)).ServeHTTP(w, r) } ================================================ FILE: ui/backend/uiserver/plgnstats.go ================================================ package uiserver import ( "encoding/json" "net/http" "github.com/aquasecurity/postee/ui/backend/dbservice" ) func (srv *uiServer) plgnStats(w http.ResponseWriter, r *http.Request) { stats, err := dbservice.GetPlgnStats() if err != nil { handleErr(w, err) return } data, err := json.Marshal(stats) if err != nil { handleErr(w, err) return } w.WriteHeader(http.StatusOK) w.Write([]byte(data)) } func handleErr(w http.ResponseWriter, err error) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(err.Error())) } ================================================ FILE: ui/backend/uiserver/server.go ================================================ package uiserver import ( "log" "net/http" "github.com/gorilla/mux" "github.com/gorilla/securecookie" "github.com/gorilla/sessions" ) type uiServer struct { port string cfgPath string boltDbPath string webhookUrl string updateKey string admusr string admpwd string router *mux.Router store *sessions.CookieStore } func Instance(webLocalPath, port, cfg, webhookUrl, admusr string, admpwd string) *uiServer { server := &uiServer{ port: port, cfgPath: cfg, webhookUrl: webhookUrl, admusr: admusr, admpwd: admpwd, router: mux.NewRouter().StrictSlash(true), } authKeyOne := securecookie.GenerateRandomKey(64) encryptionKeyOne := securecookie.GenerateRandomKey(32) server.store = sessions.NewCookieStore( authKeyOne, encryptionKeyOne, ) server.store.Options = &sessions.Options{ MaxAge: 60 * 60 * 24, //one day HttpOnly: true, } server.router.Use(server.authenticationMiddleware) server.router.HandleFunc("/api/login", server.login).Methods("POST") server.router.HandleFunc("/api/logout", server.logout).Methods("GET") server.router.HandleFunc("/api/config", server.updateConfig).Methods("POST") server.router.HandleFunc("/api/config", server.getConfig).Methods("GET") server.router.HandleFunc("/api/test", server.testSettings).Methods("POST") server.router.HandleFunc("/api/actions/stats", server.plgnStats).Methods("GET") server.router.HandleFunc("/api/events", server.getEvents).Methods("GET") server.router.HandleFunc("/ping", server.pingHandler).Methods("GET") web := &localWebServer{ localPath: webLocalPath, url: "/", } server.router.PathPrefix("/").Handler(web) return server } func (srv *uiServer) Start() { log.Print("UI Postee server starting...") http.ListenAndServe(":"+srv.port, srv.router) } func (srv *uiServer) Stop() { log.Print("UI Postee server stopped!") } func (ctx *uiServer) pingHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) } ================================================ FILE: ui/backend/uiserver/testplg.go ================================================ package uiserver import ( "encoding/json" "fmt" "io" "log" "net/http" "github.com/aquasecurity/postee/v2/layout" "github.com/aquasecurity/postee/v2/router" ) func (srv *uiServer) testSettings(w http.ResponseWriter, r *http.Request) { plgSettings := &router.ActionSettings{} defer r.Body.Close() body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Invalid request", http.StatusBadRequest) return } if err := json.Unmarshal(body, plgSettings); err != nil { http.Error(w, fmt.Sprintf("Can't read JSON string %s", err), http.StatusBadRequest) return } plg := router.BuildAndInitOtpt(plgSettings, "") testPayload := make(map[string]string) testPayload["title"] = "Postee test title" testPayload["description"] = layout.GenTestDescription(plg.GetLayoutProvider(), "Postee test description") log.Printf("description is: %s \n", testPayload["description"]) err = plg.Send(testPayload) if err != nil { //TODO provide method to write error response as JSON http.Error(w, fmt.Sprintf("Can't test output: %s \n", err), http.StatusBadRequest) return } } ================================================ FILE: ui/backend/uiserver/update_test.go ================================================ package uiserver import ( "io" "net/http" "net/http/httptest" "os" "strings" "testing" ) const ( testCfgFile = "test.cfg" inputConfigJson = `[{"type":"common"}]` ) func TestUpdateConfig(t *testing.T) { tests := []struct { input string status int }{ {inputConfigJson, http.StatusOK}, } srv := &uiServer{ cfgPath: testCfgFile, } os.Create(testCfgFile) defer os.RemoveAll(testCfgFile) for _, test := range tests { req := httptest.NewRequest("POST", "/update", strings.NewReader(test.input)) w := httptest.NewRecorder() srv.updateConfig(w, req) response := w.Result() msg, err := io.ReadAll(response.Body) if err != nil { panic(err) } if st := w.Result().StatusCode; st != test.status { t.Errorf("request to /update returns a wrong status %d, wanted %d.\nData: %q\nMessage: %q", st, test.status, test.input, string(msg)) } response.Body.Close() } } ================================================ FILE: ui/frontend/.gitignore ================================================ .DS_Store node_modules /dist # local env files .env.local .env.*.local # Log files npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* # Editor directories and files .idea .vscode *.suo *.ntvs* *.njsproj *.sln *.sw? ================================================ FILE: ui/frontend/README.md ================================================ # postee-ui ## Project setup ``` yarn install ``` ### Compiles and hot-reloads for development ``` yarn serve ``` ### Compiles and minifies for production ``` yarn build ``` ### Lints and fixes files ``` yarn lint ``` ### Customize configuration See [Configuration Reference](https://cli.vuejs.org/config/). ================================================ FILE: ui/frontend/babel.config.js ================================================ module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ] } ================================================ FILE: ui/frontend/package.json ================================================ { "name": "postee-ui", "version": "0.2.0", "private": true, "scripts": { "serve": "vue-cli-service serve", "build": "vue-cli-service build", "lint": "vue-cli-service lint" }, "dependencies": { "axios": "^0.21.1", "bootstrap-vue": "^2.21.2", "codemirror-rego": "^1.1.0", "core-js": "^3.18.1", "js-yaml": "^4.1.0", "node-forge": "^1.3.0", "vue": "^2.6.11", "vue-codemirror": "^4.0.6", "vue-json-pretty": "^1.8.3", "vue-json-viewer": "2", "vue-router": "^3.5.1", "vue-tmx": "^0.1.12", "vue-tour": "^2.0.0", "vuex": "^3.6.2" }, "devDependencies": { "@vue/cli-plugin-babel": "^5.0.6", "@vue/cli-plugin-eslint": "^5.0.6", "@vue/cli-service": "^5.0.8", "babel-eslint": "^10.1.0", "eslint": "^7.32.0", "eslint-plugin-vue": "^7.18.0", "vue-template-compiler": "^2.6.11" }, "resolutions": { "ansi-regex": "5.0.1", "glob-parent": "5.1.2", "nth-check": "2.0.1", "node-forge": "1.3.0" }, "eslintConfig": { "root": true, "env": { "node": true }, "extends": [ "plugin:vue/essential", "eslint:recommended" ], "parserOptions": { "parser": "babel-eslint" }, "rules": {} }, "browserslist": [ "> 1%", "last 2 versions", "not dead" ] } ================================================ FILE: ui/frontend/public/index.html ================================================ <%= htmlWebpackPlugin.options.title %>
================================================ FILE: ui/frontend/src/App.vue ================================================ ================================================ FILE: ui/frontend/src/api.js ================================================ import axios from "axios"; import yaml from "js-yaml"; const transformYaml = (response) => { try { const json = yaml.load(response) return json } catch(e) { return response //this way text errors are handled } } export default { getConfig: function () { return axios.get("/api/config", { transformResponse: transformYaml }) }, getStats: function () { return axios.get("/api/actions/stats") }, getEvents: function() { return axios.get("/api/events") }, saveConfig: function (settings) { const yamlObj = yaml.dump(settings) return axios.post("/api/config", yamlObj) }, test: function (settings) { return axios.post("/api/test", settings) }, login: function (username, password) { const bodyFormData = new FormData(); bodyFormData.append('username', username ? username : ""); bodyFormData.append('password', password ? password : ""); return axios.post("/api/login", bodyFormData) }, logout: function () { return axios.get("/api/logout") }, toApiPayload: function (context, modification) { const rootState = context.rootGetters.getAppState return { actions: rootState.actions.all, routes: rootState.routes.all, templates: rootState.templates.all, rules: rootState.rules.all, ...rootState.settings.all, ...modification } } } ================================================ FILE: ui/frontend/src/components/ActionCard.vue ================================================ ================================================ FILE: ui/frontend/src/components/ActionDetails.vue ================================================ ================================================ FILE: ui/frontend/src/components/Actions.vue ================================================ ================================================ FILE: ui/frontend/src/components/CheckboxPropertyField.vue ================================================ ================================================ FILE: ui/frontend/src/components/EventDetails.vue ================================================ ================================================ FILE: ui/frontend/src/components/LoginForm.vue ================================================ ================================================ FILE: ui/frontend/src/components/PropertyField.vue ================================================ ================================================ FILE: ui/frontend/src/components/RouteCard.vue ================================================ ================================================ FILE: ui/frontend/src/components/RouteDetails.vue ================================================ ================================================ FILE: ui/frontend/src/components/Routes.vue ================================================ ================================================ FILE: ui/frontend/src/components/Settings.vue ================================================ ================================================ FILE: ui/frontend/src/components/TemplateCard.vue ================================================ ================================================ FILE: ui/frontend/src/components/TemplateDetails.vue ================================================ ================================================ FILE: ui/frontend/src/components/Templates.vue ================================================ ================================================ FILE: ui/frontend/src/components/form.js ================================================ export default { methods: { updateField(e) { const propName = e.target.attributes["name"].value; const inputType = e.target.attributes["type"]?.value; let v switch(inputType) { case "checkbox": { v = e.target.checked break; } case "number": { v = Number(e.target.value) break; } default: { v = e.target.value } } this.formValues[propName] = v; }, updateCollectionField(e) { const propName = e.target.attributes["name"].value; const v = e.target.value.split(",").map((s) => s.trim()); this.formValues[propName] = v; }, isFormValid() { let firstElement; this.errors = {} for (const id in this.fields) { const validator = this.fields[id]; const fieldValidations = Array.isArray(validator.validationFn)?validator.validationFn:[validator.validationFn]; const element = document.getElementById(id); if (element) { //only elements in DOM are validated /*validator functions can be combined using AND*/ fieldValidations.find(vfn=>{ const r = vfn( validator.label, this.formValues[validator.name] ); if (r) { this.errors[validator.name] = r; if (firstElement === undefined) { firstElement = element; } return true } }) } } firstElement && firstElement.focus(); return Object.keys(this.errors).length === 0; } } } ================================================ FILE: ui/frontend/src/components/validator.js ================================================ class validator { constructor(fields, validationFn) { this.validationFn = validationFn this.register = (id, label, name) => { this.label = label this.name = name fields[id] = this } } } export default { methods: { url(label, value) { if (!value) { return false } const errorMsg = `Invalid url : ${value}` let url try { url = new URL(value); } catch (_) { return errorMsg; } return url.protocol === "http:" || url.protocol === "https:" ? false : errorMsg; }, email(label, value) { 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,}))$/ return re.test(String(value).toLowerCase()) ? false : `Invalid email '${value}'` }, required(label, value) { return !value ? `${label} is required` : false; }, validateJiraPasswordandToken() { if (!this.formValues.token && !this.formValues.password){ return `Password or token are required` } else if (this.formValues.token && this.formValues.password){ return `Password and token are filled` } return false; }, recipients(label, value) { const hasOneElement = value && value.length && value[0] if (!hasOneElement) { return `At least one of ${label} is required` } else { for (const email of value) { const v = this.email("-", email); if (v) { return v; } } } return false }, v(validationFn) { return new validator(this.fields, validationFn); } } } ================================================ FILE: ui/frontend/src/main.js ================================================ import Vue from 'vue' import VueRouter from 'vue-router' import App from './App.vue' import Actions from './components/Actions.vue' import LoginForm from './components/LoginForm.vue' import ActionDetails from './components/ActionDetails.vue' import RouteDetails from './components/RouteDetails.vue' import Routes from './components/Routes.vue' import TemplateDetails from './components/TemplateDetails.vue' import Templates from './components/Templates.vue' import Settings from './components/Settings.vue' import Events from './components/EventDetails.vue' import { BootstrapVue, BootstrapVueIcons } from 'bootstrap-vue' import store from './store/store' import 'bootstrap/dist/css/bootstrap.css' import 'bootstrap-vue/dist/bootstrap-vue.css' import VueTour from 'vue-tour' require('vue-tour/dist/vue-tour.css') Vue.use(BootstrapVue); Vue.use(BootstrapVueIcons); Vue.use(VueRouter); Vue.use(VueTour); const routes = [ { name: 'home', path: '/', redirect: '/routes' }, { name: 'actions', path: '/actions', component: Actions }, { name: 'routes', path: '/routes', component: Routes }, { name: 'add-route', path: '/route', component: RouteDetails }, { name: 'route', path: '/route/:name', component: RouteDetails }, { name: 'settings', path: '/settings', component: Settings }, { name: 'login', path: '/login', component: LoginForm }, { name: 'add-action', path: '/action', component: ActionDetails }, { name: 'action', path: '/action/:name', component: ActionDetails }, { name: 'events', path: '/events', component: Events }, { name: 'templates', path: '/templates', component: Templates }, { name: 'add-template', path: '/template', component: TemplateDetails }, { name: 'template', path: '/template/:name', component: TemplateDetails } ]; export const router = new VueRouter({ routes, mode: 'history' }); new Vue({ router, store, render: h => h(App), }).$mount('#app') ================================================ FILE: ui/frontend/src/store/modules/account.js ================================================ import api from "../../api" import { router } from './../../main' export default { namespaced: true, state: { authenticated: false }, actions: { login(context, payload) { const { username, password } = payload || {} return new Promise((resolve, reject) => { api.login(username, password).then(() => { context.commit("update", { authenticated: true }) context.commit("error/clear", undefined, {root: true}) resolve() }).catch(error => { if (username && password) { const errorMsg = error.response.status === 401 ? "Invalid credentials" : error.response.data; context.commit("error/set", errorMsg, {root: true}) reject(errorMsg) } else { reject() //just checking } }) }) }, logout(context) { api.logout().then(() => { context.commit("update", { authenticated: false }) router.push({ name: "login" }); }).catch(error => { context.commit("error/set", error.response.data, {root: true}) }) }, }, mutations: { update(state, info) { state.userInfo = { ...info } }, } } ================================================ FILE: ui/frontend/src/store/modules/actions.js ================================================ import api from "../../api" function updateActions(context, actions) { api.saveConfig(api.toApiPayload(context, {actions})).then( //entire config is saved context.commit("set", actions) ).catch((error) => { context.commit("error/set", error.response.data, {root:true}) }) } export default { namespaced: true, state: {all: []}, actions: { test(context, settings) { return new Promise((resolve, reject) => { api.test(settings).then(() => { context.commit("error/clear", undefined, {root:true}) resolve(); }).catch(error => { if (error.response) { context.commit("error/set", error.response.data, {root:true}) reject(error.response.data); } else { console.error(error) reject(error); } }) }) }, update(context, payload) { const actions = context.state.all; const { value, name } = payload for (let i = 0; i < actions.length; i++) { if (actions[i].name == name) { actions.splice(i, 1, value) } } updateActions(context, actions) }, remove(context, name) { const actions = context.state.all.filter(item => item.name != name) updateActions(context, actions) }, add(context, settings) { const actions = context.state.all actions.push(settings) updateActions(context, actions) }, }, mutations: { set(state, actions) { state.all = [...actions] }, } } ================================================ FILE: ui/frontend/src/store/modules/error.js ================================================ export default { namespaced: true, state: { message: undefined }, actions: { }, mutations: { set(state, error) { state.message = error }, clear(state) { state.message = undefined }, } } ================================================ FILE: ui/frontend/src/store/modules/events.js ================================================ import api from "../../api" export default { namespaced: true, state: { all: {} }, actions: { load(context) { api.getEvents().then((response) => { context.commit("set", response.data) }).catch((error) => { context.commit("error/set", error.response.data, {root: true}) }) }, }, mutations: { set(state, payload) { state.all = { ...payload } }, } } ================================================ FILE: ui/frontend/src/store/modules/flags.js ================================================ export default { namespaced: true, state: {all: { loaded: false }}, mutations: { set(state, flags) { state.all = {...state.all, ...flags} } } } ================================================ FILE: ui/frontend/src/store/modules/routes.js ================================================ import api from "../../api" function updateRoutes(context, routes) { api.saveConfig(api.toApiPayload(context, {routes})).then( context.commit("set", routes) ).catch((error) => { context.commit("error/set", error.response.data, {root:true}) }) } export default { namespaced: true, state: {all: []}, actions: { update(context, payload) { const routes = context.state.all; const { value, name } = payload for (let i = 0; i < routes.length; i++) { if (routes[i].name == name) { routes.splice(i, 1, value) } } updateRoutes(context, routes) }, remove(context, name) { const routes = context.state.all.filter(item => item.name != name) updateRoutes(context, routes) }, add(context, settings) { const routes = context.state.all routes.push(settings) updateRoutes(context, routes) }, }, mutations: { set(state, routes) { state.all = [...routes] }, } } ================================================ FILE: ui/frontend/src/store/modules/rules.js ================================================ import api from "../../api" function updateRules(context, rules) { api.saveConfig(api.toApiPayload(context, {rules})).then( context.commit("set", rules) ).catch((error) => { context.commit("error/set", error.response.data, {root:true}) }) } export default { namespaced: true, state: {all: []}, actions: { update(context, payload) { const rules = context.state.all; const { value, name } = payload for (let i = 0; i < rules.length; i++) { if (rules[i].name == name) { rules.splice(i, 1, value) } } updateRules(context, rules) }, remove(context, name) { const rules = context.state.all.filter(item => item.name != name) updateRules(context, rules) }, add(context, settings) { const rules = context.state.all rules.push(settings) updateRules(context, rules) }, }, mutations: { set(state, rules) { state.all = [...rules] }, } } ================================================ FILE: ui/frontend/src/store/modules/settings.js ================================================ import api from "../../api" export default { namespaced: true, state: {all: {}}, actions: { update(context, payload) { api.saveConfig(api.toApiPayload(context, payload)).then( //entire config is saved context.commit("set", payload) ).catch((error) => { context.commit("error/set", error.response.data, {root: true}) }) }, }, mutations: { set(state, settings) { state.all = {...settings} }, } } ================================================ FILE: ui/frontend/src/store/modules/stats.js ================================================ import api from "../../api" export default { namespaced: true, state: { all: {} }, actions: { load(context) { api.getStats().then((response) => { context.commit("set", response.data) }).catch((error) => { context.commit("error/set", error.response.data, {root: true}) }) }, }, mutations: { set(state, payload) { state.all = { ...payload } }, } } ================================================ FILE: ui/frontend/src/store/modules/templates.js ================================================ import api from "../../api" function updateTemplates(context, templates) { api.saveConfig(api.toApiPayload(context, {templates})).then( context.commit("set", templates) ).catch((error) => { context.commit("error/set", error.response.data, {root:true}) }) } export default { namespaced: true, state: {all: []}, actions: { update(context, payload) { const templates = context.state.all; const { value, name } = payload for (let i = 0; i < templates.length; i++) { if (templates[i].name == name) { templates.splice(i, 1, value) } } updateTemplates(context, templates) }, remove(context, name) { const templates = context.state.all.filter(item => item.name != name) updateTemplates(context, templates) }, add(context, settings) { const templates = context.state.all templates.push(settings) updateTemplates(context, templates) }, }, mutations: { set(state, templates) { state.all = [...templates] }, } } ================================================ FILE: ui/frontend/src/store/store.js ================================================ import Vue from 'vue' import Vuex from 'vuex' import api from './../api' import error from './modules/error.js' import account from './modules/account.js' import actions from './modules/actions.js' import stats from './modules/stats.js' import events from './modules/events.js' import routes from './modules/routes.js' import settings from './modules/settings.js' import flags from './modules/flags.js' import templates from './modules/templates.js' import rules from './modules/rules.js' Vue.use(Vuex) export default new Vuex.Store({ modules: { error, actions, account, stats, events, routes, settings, flags, templates, rules }, getters: { getAppState(state) { return state } }, actions: { load(context) { api.getConfig().then((response) => { const data = response.data // console.log(data.rules) const settings = { name: data.name, AquaServer: data.AquaServer, Delete_Old_Data: data.Delete_Old_Data, DbVerifyInterval: data.DbVerifyInterval, Max_DB_Size: data.Max_DB_Size } data.actions && context.commit("actions/set", data.actions) data.routes && context.commit("routes/set", data.routes) data.templates && context.commit("templates/set", data.templates) data.events && context.commit("events/set", data.events) data.rules && context.commit("rules/set", data.rules) context.commit("settings/set", settings) context.commit("flags/set", { loaded: true }) }).catch((error) => { if (error.response) { context.commit("error/set", error.response.data) } else { console.error(error) } }) } } }) ================================================ FILE: ui/frontend/vue.config.js ================================================ // vue.config.js /** * @type {import('@vue/cli-service').ProjectOptions} */ module.exports = { devServer: { proxy: 'http://localhost:8090' } } ================================================ FILE: utils/cert.go ================================================ package utils import ( "crypto/ecdsa" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "math/big" "net" "os" "time" ) // Generate a self-signed X.509 certificate. // Outputs will overwrite existing files. func publicKey(priv interface{}) interface{} { switch k := priv.(type) { case *rsa.PrivateKey: return &k.PublicKey case *ecdsa.PrivateKey: return &k.PublicKey default: return nil } } func pemBlockForKey(priv interface{}) *pem.Block { switch k := priv.(type) { case *rsa.PrivateKey: return &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} case *ecdsa.PrivateKey: b, err := x509.MarshalECPrivateKey(k) if err != nil { return nil } return &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} default: return nil } } func generateCertificate(hosts []string, keyFile string, certFile string) error { var priv interface{} var err error priv, err = rsa.GenerateKey(rand.Reader, 2048) if err != nil { return err } notBefore := time.Now() notAfter := notBefore.Add(3650 * 24 * time.Hour) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return err } template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ Organization: []string{"Aquasec"}, }, NotBefore: notBefore, NotAfter: notAfter, KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } for _, h := range hosts { if ip := net.ParseIP(h); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { template.DNSNames = append(template.DNSNames, h) } } template.IsCA = true template.KeyUsage |= x509.KeyUsageCertSign derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv) if err != nil { return err } certOut, err := os.Create(certFile) if err != nil { return err } err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) if err != nil { return err } certOut.Close() keyOut, err := os.OpenFile(keyFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) if err != nil { return err } err = pem.Encode(keyOut, pemBlockForKey(priv)) if err != nil { return err } keyOut.Close() return nil } func getHostnames() ([]string, error) { var hosts []string ifaces, err := net.Interfaces() if err != nil { return hosts, err } for _, iface := range ifaces { if iface.Flags&net.FlagUp == 0 { continue // interface down } if iface.Flags&net.FlagLoopback != 0 { continue // loopback interface } addrs, err := iface.Addrs() if err != nil { return hosts, err } for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip == nil || ip.IsLoopback() { continue } ip = ip.To4() if ip == nil { continue // not an ipv4 address } hosts = append(hosts, ip.String()) hh, _ := net.LookupAddr(ip.String()) for _, h := range hh { hosts = append(hosts, h) } } } return hosts, nil } func GenerateCertificate(keyFile string, certFile string) error { hosts, err := getHostnames() if err != nil { return err } return generateCertificate(hosts, keyFile, certFile) } ================================================ FILE: utils/prnheaders.go ================================================ package utils import ( "io" "io/ioutil" ) func PrnLogResponse(body io.ReadCloser) string { defer body.Close() message, _ := ioutil.ReadAll(body) return string(message) } ================================================ FILE: utils/utils.go ================================================ package utils import ( "errors" "log" "os" "path/filepath" "strings" ) var ( dbg = false ) func GetEnvironmentVarOrPlain(value string) string { const VarPrefix = "$" if strings.HasPrefix(value, VarPrefix) { return os.Getenv(strings.TrimPrefix(value, VarPrefix)) } return value } func InitDebug() { if os.Getenv("AQUAALERT_DEBUG") != "" { dbg = true } if os.Getenv("POSTEE_DEBUG") != "" { dbg = true } } func Debug(format string, v ...interface{}) { if dbg != false { log.Printf(format, v...) } } func GetEnv(name string) (string, error) { value := os.Getenv(name) if len(value) > 0 { return value, nil } return "", errors.New("not found") } // GetRootDir returns the full path of the directory in which the process // is running. func GetRootDir() (string, error) { return filepath.Abs(filepath.Dir(os.Args[0])) } // PathExists checks if a (full) path exists on the host/container. func PathExists(name string) bool { _, err := os.Stat(name) return !os.IsNotExist(err) } ================================================ FILE: webserver/reload.go ================================================ package webserver import ( "net/http" "github.com/aquasecurity/postee/v2/router" ) func (web *WebServer) reload(w http.ResponseWriter, r *http.Request) { router.Instance().ReloadConfig() } ================================================ FILE: webserver/tenant.go ================================================ package webserver import ( "io/ioutil" "log" "net/http" "github.com/aquasecurity/postee/v2/router" "github.com/aquasecurity/postee/v2/utils" "github.com/gorilla/mux" ) func (ctx *WebServer) tenantHandler(w http.ResponseWriter, r *http.Request) { route, ok := mux.Vars(r)["route"] if !ok || len(route) == 0 { log.Printf("Failed route: %q", route) ctx.writeResponse(w, http.StatusBadRequest, "failed route") return } body, err := ioutil.ReadAll(r.Body) if err != nil { log.Printf("Failed ioutil.ReadAll: %s", err) ctx.writeResponseError(w, http.StatusInternalServerError, err) return } defer r.Body.Close() utils.Debug("%s\n\n", string(body)) router.Instance().HandleRoute(route, body) ctx.writeResponse(w, http.StatusOK, "") } ================================================ FILE: webserver/webserver.go ================================================ package webserver import ( "encoding/json" "io/ioutil" "log" "net/http" "os" "path/filepath" "strings" "sync" "github.com/aquasecurity/postee/v2/dbservice" "github.com/aquasecurity/postee/v2/router" "github.com/aquasecurity/postee/v2/utils" "github.com/gorilla/mux" ) type WebServer struct { quit chan struct{} router *mux.Router } var initCtx sync.Once var wsCtx *WebServer func Instance() *WebServer { initCtx.Do(func() { wsCtx = &WebServer{ quit: make(chan struct{}), router: mux.NewRouter().StrictSlash(true), } }) return wsCtx } func (ctx *WebServer) withApiKey(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { correctKey, err := dbservice.GetApiKey() if err != nil || correctKey == "" { log.Printf("reload API key is either empty or there is an error: %s \n", err) http.Error(w, "Unauthorized", http.StatusUnauthorized) } if key := r.URL.Query().Get("key"); key != correctKey { log.Printf("reload API received an incorrect key %q", key) http.Error(w, "Unauthorized", http.StatusUnauthorized) return } next(w, r) } } func (ctx *WebServer) Start(host, tlshost string) { log.Printf("Starting WebServer....") rootDir, _ := utils.GetRootDir() certPem := filepath.Join(rootDir, "cert.pem") keyPem := filepath.Join(rootDir, "key.pem") if ok := utils.PathExists(keyPem); ok != true { err := utils.GenerateCertificate(keyPem, certPem) if err != nil { log.Printf("GenerateCertificate error: %v \n", err) } } if os.Getenv("AQUAALERT_CERT_PEM") != "" { certPem = os.Getenv("AQUAALERT_CERT_PEM") } if os.Getenv("AQUAALERT_KEY_PEM") != "" { keyPem = os.Getenv("AQUAALERT_KEY_PEM") } err := dbservice.EnsureApiKey() if err != nil { log.Printf("EnsureApiKey error: %v \n", err) } ctx.router.HandleFunc("/", ctx.sessionHandler(ctx.scanHandler)).Methods("POST") ctx.router.HandleFunc("/tenant/{route}", ctx.sessionHandler(ctx.tenantHandler)).Methods("POST") ctx.router.HandleFunc("/scan", ctx.sessionHandler(ctx.scanHandler)).Methods("POST") ctx.router.HandleFunc("/ping", ctx.sessionHandler(ctx.pingHandler)).Methods("GET") ctx.router.HandleFunc("/events", ctx.sessionHandler(ctx.eventsHandler)).Methods("GET") ctx.router.HandleFunc("/reload", ctx.withApiKey(ctx.reload)).Methods("GET") go func() { log.Printf("Listening for HTTP on %s ", host) log.Fatal(http.ListenAndServe(host, ctx.router)) }() go func() { log.Printf("Listening for HTTPS on %s", tlshost) log.Fatal(http.ListenAndServeTLS(tlshost, certPem, keyPem, ctx.router)) }() } func (ctx *WebServer) Terminate() { log.Printf("Terminating WebServer....") close(ctx.quit) } func (ctx *WebServer) sessionHandler(f func(http.ResponseWriter, *http.Request)) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { f(w, r) } } func (ctx *WebServer) scanHandler(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { log.Printf("Failed ioutil.ReadAll: %s\n", err) ctx.writeResponseError(w, http.StatusInternalServerError, err) return } defer r.Body.Close() utils.Debug("%s\n\n", string(body)) router.Instance().Send(body) ctx.writeResponse(w, http.StatusOK, "") } func (ctx *WebServer) pingHandler(w http.ResponseWriter, r *http.Request) { ctx.writeResponse(w, http.StatusOK, "Postee alive!") } func (ctx *WebServer) writeResponse(w http.ResponseWriter, httpStatus int, v interface{}) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(httpStatus) if v != nil { result, _ := json.Marshal(v) _, err := w.Write(result) if err != nil { log.Printf("Write error: %s \n", err) } } } func (ctx *WebServer) writeResponseError(w http.ResponseWriter, httpError int, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(httpError) errEncode := json.NewEncoder(w).Encode(err) if errEncode != nil { log.Printf("Encode error: %s \n", errEncode) } } func (ctx *WebServer) eventsHandler(w http.ResponseWriter, r *http.Request) { var events []byte events = append(events, []byte("[")...) currentEvents := router.Instance().GetCurrentEvents() if len(currentEvents) > 0 && currentEvents != nil { for i, ce := range currentEvents { if ce != nil { if i < len(currentEvents)-1 { events = append(events, []byte(strings.Join([]string{string(ce.([]byte)), ","}, ""))...) } else { events = append(events, ce.([]byte)...) } } } } events = append(events, []byte("]")...) w.WriteHeader(http.StatusOK) _, _ = w.Write(events) } ================================================ FILE: webserver/webserver_test.go ================================================ package webserver import ( "io/ioutil" "net/http" "net/http/httptest" "testing" "github.com/aquasecurity/postee/v2/router" "github.com/stretchr/testify/assert" ) func TestWebServer_eventsHandler(t *testing.T) { rtr := router.Instance() rtr.Send([]byte(`{"SigMetadata":{"ID":"TRC-2", "hostname":"postee-0"}}`)) rtr.Send([]byte(`{"SigMetadata":{"ID":"TRC-3", "hostname":"postee-0"}}`)) ws := WebServer{} w := httptest.NewRecorder() var r *http.Request ws.eventsHandler(w, r) resp := w.Result() defer func() { _ = resp.Body.Close() }() got, _ := ioutil.ReadAll(resp.Body) assert.JSONEq(t, `[ { "SigMetadata":{ "ID":"TRC-2", "hostname":"postee-0" } }, { "SigMetadata":{ "ID":"TRC-3", "hostname":"postee-0" } } ]`, string(got)) }