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]

[![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:
[](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.

## 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:

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:

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.

| 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:

| 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.

| 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.

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**() > **Issues** > **Custom fields** under the Fields section: 
3. Click on the required field. 
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:

## HTTP

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

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

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.

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.

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.

## 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.

## 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.

## 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.

## 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.

## 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.

================================================
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.

## 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.

## 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:

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:

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.

| 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.

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
================================================
#
{ 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.

================================================
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.

!!! 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.

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

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.

## 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
CRITICAL HIGH MEDIUM LOW NEGLIGIBLE
0 1 3 4 5
`
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
description1title2
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%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: `
Header1 Header2
Field1 Field2
`,
},
}
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:=`
`
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:=`
`
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:=`
`
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:=`
`
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 %>
We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.
================================================
FILE: ui/frontend/src/App.vue
================================================
Postee
Routes
Actions
Templates
Settings
Events
Logout
================================================
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
================================================
{{ isCommon ? "Defaults" : name }}
{{ scanCountMessage }}
================================================
FILE: ui/frontend/src/components/ActionDetails.vue
================================================
Submit
Test config
Remove
================================================
FILE: ui/frontend/src/components/Actions.vue
================================================
Add Action
An Action is a enforceable step that enables the Postee operator to act upon incoming events.
================================================
FILE: ui/frontend/src/components/CheckboxPropertyField.vue
================================================
{{ label }}
{{
description
}}
================================================
FILE: ui/frontend/src/components/EventDetails.vue
================================================
All incoming events that Postee has received so far.
================================================
FILE: ui/frontend/src/components/LoginForm.vue
================================================
================================================
FILE: ui/frontend/src/components/PropertyField.vue
================================================
================================================
FILE: ui/frontend/src/components/RouteCard.vue
================================================
{{ actionCnt }} actions configured
================================================
FILE: ui/frontend/src/components/RouteDetails.vue
================================================
================================================
FILE: ui/frontend/src/components/Routes.vue
================================================
Add Route
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.
================================================
FILE: ui/frontend/src/components/Settings.vue
================================================
Submit
Modify current Postee settings.
================================================
FILE: ui/frontend/src/components/TemplateCard.vue
================================================
================================================
FILE: ui/frontend/src/components/TemplateDetails.vue
================================================
================================================
FILE: ui/frontend/src/components/Templates.vue
================================================
Add Template
Templates are used to format input messages before sending them to the action.
================================================
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))
}