Showing preview only (1,142K chars total). Download the full file or copy to clipboard to get everything.
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
<!--
Briefly describe the problem you are having in a few paragraphs.
-->
## 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
<!--Describe what your feature would look like
The more information you add as part of your abstract,
the easier it will be understand-->
# Use Case
<!--Add use cases for your feature that demonstrate the need for it
as a template you could use the following while writing a use case:
As a User of X,
I would like to see Y,
so that I can accomplish Z
-->
# Acceptance Criteria
<!--Define what it would mean for this feature to fully marked as done
and add value to you. An example:
When feature XYZ is implemented, I will be able to do ABC task-->
================================================
FILE: .github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md
================================================
---
name: Support Question
labels: triage/support
about: If you have a question about Postee.
---
<!--
If you have a trouble, feel free to ask.
Make sure you're not asking duplicate question by searching on the issues lists.
-->
================================================
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.
<p align="center">
<img src="./docs/img/postee.png">
</p>
![Docker Pulls][docker-pull]
[![Go Report Card][report-card-img]][report-card]

[![License][license-img]][license]
<a href="https://slack.aquasec.com/?_ga=2.51428586.2119512742.1655808394-1739877964.1641199050">
<img src="https://img.shields.io/static/v1?label=Slack&message=Join+our+Community&color=4a154b&logo=slack">
</a>
[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: `<p>This message is too long to display here. Please visit the link to read the content.</p>
<p><a href='foo1.com'>foo1.com</a></p>
`,
},
{
name: "no configured aqua server",
inputUrls: "foo1.com",
provider: new(formatting.HtmlProvider),
want: `<p>Please configure Aqua server url to get link to entire scan results.</p>
<p><a href='https://aquasecurity.github.io/postee/settings/'>Postee settings</a></p>
`,
},
{
name: "no configured urls",
inputServer: "foo.com",
provider: new(formatting.HtmlProvider),
want: `<p>Unable to create link to entire scan results. Input message doesn't contain 'registry' and 'image' fields or they are empty</p>
`,
},
}
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
================================================
<?xml version="1.0"?>
<bom serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79" version="1"
xmlns="http://cyclonedx.org/schema/bom/1.1"
xmlns:v="http://cyclonedx.org/schema/ext/vulnerability/1.0">
<components>
<component type="application">
<name>musl</name>
<version>1.1.18-r3</version>
<licenses>
<license>
<id>MIT</id>
</license>
</licenses>
<v:vulnerabilities>
<v:vulnerability>
<v:id>CVE-2019-14697</v:id>
<v:source name="NVD">
<v:url>https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2019-14697</v:url>
</v:source>
<v:ratings>
<v:rating>
<v:score>
<v:base>9.8</v:base>
<v:impact>9.8</v:impact>
<v:exploitability>9.8</v:exploitability>
</v:score>
<v:severity>critical</v:severity>
<v:method>CVSS V3</v:method>
<v:vector>CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H</v:vector>
</v:rating>
</v:ratings>
<v:recommendations>
<v:recommendation>Upgrade package musl to version 1.1.18-r4 or above.</v:recommendation>
</v:recommendations>
</v:vulnerability>
</v:vulnerabilities>
</components>
</bom>
================================================
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. <numbers><unit suffix> 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/<key>
- 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. <numbers><unit suffix> 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: <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: <API Key> # 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. <numbers><unit suffix> 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. <numbers><unit suffix> 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/<uuid>" # 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/<uuid>" # 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. <numbers><unit suffix> 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-
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
SYMBOL INDEX (631 symbols across 148 files)
FILE: actions/aws_securityhub.go
type securityHubAPI (line 18) | type securityHubAPI interface
type Finding (line 22) | type Finding struct
type Report (line 70) | type Report struct
type AWSSecurityHubClient (line 74) | type AWSSecurityHubClient struct
method GetName (line 80) | func (sh AWSSecurityHubClient) GetName() string {
method Init (line 84) | func (sh *AWSSecurityHubClient) Init() error {
method Send (line 99) | func (sh AWSSecurityHubClient) Send(m map[string]string) error {
method Terminate (line 170) | func (sh AWSSecurityHubClient) Terminate() error {
method GetLayoutProvider (line 174) | func (sh AWSSecurityHubClient) GetLayoutProvider() layout.LayoutProvid...
function chunkBy (line 180) | func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {
FILE: actions/aws_securityhub_test.go
constant GoodFindings (line 16) | GoodFindings = `{
type mockAWSSHClient (line 123) | type mockAWSSHClient struct
method BatchImportFindings (line 129) | func (mc mockAWSSHClient) BatchImportFindings(ctx context.Context, par...
function TestAWSSecurityHubClient_Send (line 136) | func TestAWSSecurityHubClient_Send(t *testing.T) {
FILE: actions/dependencytrack.go
type DependencyTrackAction (line 16) | type DependencyTrackAction struct
method GetName (line 22) | func (dta *DependencyTrackAction) GetName() string {
method Init (line 26) | func (dta *DependencyTrackAction) Init() error {
method Send (line 31) | func (dta *DependencyTrackAction) Send(content map[string]string) error {
method Terminate (line 70) | func (dta *DependencyTrackAction) Terminate() error {
method GetLayoutProvider (line 75) | func (dta *DependencyTrackAction) GetLayoutProvider() layout.LayoutPro...
FILE: actions/dependencytrack_test.go
function TestDependencyTrackAction_Send (line 12) | func TestDependencyTrackAction_Send(t *testing.T) {
FILE: actions/docker.go
type DockerClient (line 22) | type DockerClient struct
method GetName (line 34) | func (d DockerClient) GetName() string {
method Init (line 38) | func (d *DockerClient) Init() error {
method Send (line 50) | func (d DockerClient) Send(m map[string]string) error {
method Terminate (line 109) | func (d DockerClient) Terminate() error {
method GetLayoutProvider (line 117) | func (d DockerClient) GetLayoutProvider() layout.LayoutProvider {
method parseCmd (line 121) | func (d DockerClient) parseCmd(input map[string]string) (parsedCmds []...
FILE: actions/docker_test.go
type mockDockerClient (line 21) | type mockDockerClient struct
method ImagePull (line 32) | func (m mockDockerClient) ImagePull(ctx context.Context, ref string, o...
method ContainerCreate (line 40) | func (m mockDockerClient) ContainerCreate(ctx context.Context, config ...
method ContainerStart (line 50) | func (m mockDockerClient) ContainerStart(ctx context.Context, containe...
method ContainerWait (line 58) | func (m mockDockerClient) ContainerWait(ctx context.Context, container...
method ContainerLogs (line 77) | func (m mockDockerClient) ContainerLogs(ctx context.Context, container...
method ContainerRemove (line 85) | func (m mockDockerClient) ContainerRemove(ctx context.Context, contain...
type mockUUID (line 93) | type mockUUID struct
method New (line 96) | func (mockUUID) New() uuid.UUID {
function TestDocketClient_Send (line 100) | func TestDocketClient_Send(t *testing.T) {
FILE: actions/email.go
type EmailAction (line 22) | type EmailAction struct
method GetName (line 35) | func (email *EmailAction) GetName() string {
method Init (line 39) | func (email *EmailAction) Init() error {
method Terminate (line 54) | func (email *EmailAction) Terminate() error {
method GetLayoutProvider (line 59) | func (email *EmailAction) GetLayoutProvider() layout.LayoutProvider {
method Send (line 63) | func (email *EmailAction) Send(content map[string]string) error {
method sendEmailWithCustomClient (line 102) | func (email EmailAction) sendEmailWithCustomClient(addr string, a smtp...
method sendViaMxServers (line 149) | func (email EmailAction) sendViaMxServers(port string, msg string, rec...
FILE: actions/email_test.go
function mockSend (line 12) | func mockSend(errToReturn error, emailSent *int) (func(string, smtp.Auth...
type emailRecorder (line 23) | type emailRecorder struct
function TestEmailAction_Send (line 31) | func TestEmailAction_Send(t *testing.T) {
FILE: actions/exec.go
type ExecClient (line 17) | type ExecClient struct
method GetName (line 26) | func (e *ExecClient) GetName() string {
method Init (line 30) | func (e *ExecClient) Init() error {
method Send (line 35) | func (e *ExecClient) Send(m map[string]string) error {
method Terminate (line 59) | func (e *ExecClient) Terminate() error {
method GetLayoutProvider (line 64) | func (e *ExecClient) GetLayoutProvider() layout.LayoutProvider {
FILE: actions/exec_test.go
function fakeExecCmdFailure (line 14) | func fakeExecCmdFailure(command string, args ...string) *exec.Cmd {
function TestShellProcessFail (line 22) | func TestShellProcessFail(t *testing.T) {
function TestExecClient_Init (line 30) | func TestExecClient_Init(t *testing.T) {
function TestExecClient_GetName (line 35) | func TestExecClient_GetName(t *testing.T) {
function TestExecClient_Send (line 41) | func TestExecClient_Send(t *testing.T) {
FILE: actions/http.go
type HTTPClient (line 23) | type HTTPClient struct
method GetName (line 33) | func (hc *HTTPClient) GetName() string {
method Init (line 37) | func (hc *HTTPClient) Init() error {
method Send (line 41) | func (hc HTTPClient) Send(m map[string]string) error {
method Terminate (line 108) | func (hc HTTPClient) Terminate() error {
method GetLayoutProvider (line 113) | func (hc HTTPClient) GetLayoutProvider() layout.LayoutProvider {
function parseBody (line 90) | func parseBody(inputEvent map[string]string, bodyContent string) string {
FILE: actions/http_test.go
function TestHTTPClient_Init (line 14) | func TestHTTPClient_Init(t *testing.T) {
function TestHTTPClient_GetName (line 19) | func TestHTTPClient_GetName(t *testing.T) {
function TestHTTPClient_Send (line 25) | func TestHTTPClient_Send(t *testing.T) {
FILE: actions/jira.go
constant defaultIssueType (line 23) | defaultIssueType = "Task"
constant defaultIssuePriority (line 24) | defaultIssuePriority = "High"
constant defaultSprintPlugin (line 25) | defaultSprintPlugin = "com.pyxis.greenhopper.jira:gh-sprint"
constant NotConfiguredSprintId (line 26) | NotConfiguredSprintId = -1
type JiraAPI (line 29) | type JiraAPI struct
method GetName (line 53) | func (ctx *JiraAPI) GetName() string {
method fetchBoardId (line 57) | func (ctx *JiraAPI) fetchBoardId(boardName string) {
method fetchSprintId (line 88) | func (ctx *JiraAPI) fetchSprintId(client *jira.Client) {
method Terminate (line 107) | func (ctx *JiraAPI) Terminate() error {
method Init (line 112) | func (ctx *JiraAPI) Init() error {
method GetLayoutProvider (line 125) | func (jira *JiraAPI) GetLayoutProvider() layout.LayoutProvider {
method buildTransportClient (line 129) | func (ctx *JiraAPI) buildTransportClient() (*http.Client, error) {
method Send (line 172) | func (ctx *JiraAPI) Send(content map[string]string) error {
method openIssue (line 251) | func (ctx *JiraAPI) openIssue(client *jira.Client, issue *jira.Issue) ...
function createMetaProject (line 262) | func createMetaProject(c *jira.Client, project string) (*jira.MetaProjec...
function getIssueType (line 277) | func getIssueType(ctx *JiraAPI, metaProject *jira.MetaProject) (string, ...
function validateIssueType (line 296) | func validateIssueType(issueType string, metaProject *jira.MetaProject) ...
function validateIssuePriority (line 330) | func validateIssuePriority(priority string, priorityList []jira.Priority...
function createFieldsConfig (line 339) | func createFieldsConfig(ctx *JiraAPI, client *jira.Client, content *map[...
function createMetaIssueType (line 395) | func createMetaIssueType(metaProject *jira.MetaProject, issueType string...
function InitIssue (line 404) | func InitIssue(c *jira.Client, metaProject *jira.MetaProject, metaIssuet...
function findUserOnJiraServer (line 528) | func findUserOnJiraServer(c *jira.Client, email string) ([]jira.User, *j...
function isServerJira (line 540) | func isServerJira(rawUrl string) bool {
FILE: actions/jira_test.go
function TestJiraAPI_GetName (line 139) | func TestJiraAPI_GetName(t *testing.T) {
function TestJiraAPI_FetchBoardId (line 148) | func TestJiraAPI_FetchBoardId(t *testing.T) {
function TestJiraAPI_FetchSprintId (line 212) | func TestJiraAPI_FetchSprintId(t *testing.T) {
function TestJiraAPI_InitIssue (line 256) | func TestJiraAPI_InitIssue(t *testing.T) {
function TestJiraAPI_GetLayoutProvider (line 395) | func TestJiraAPI_GetLayoutProvider(t *testing.T) {
function TestJiraAPI_BuildTransportClient (line 404) | func TestJiraAPI_BuildTransportClient(t *testing.T) {
function TestJiraApi_createClient (line 446) | func TestJiraApi_createClient(t *testing.T) {
function TestJiraAPI_Send (line 481) | func TestJiraAPI_Send(t *testing.T) {
function TestJiraAPI_OpenIssue (line 587) | func TestJiraAPI_OpenIssue(t *testing.T) {
function TestJiraAPI_CreateMetaProject (line 625) | func TestJiraAPI_CreateMetaProject(t *testing.T) {
function TestJiraAPI_CreateIssueType (line 684) | func TestJiraAPI_CreateIssueType(t *testing.T) {
function TestJiraAPI_GetIssuePriority (line 738) | func TestJiraAPI_GetIssuePriority(t *testing.T) {
function TestJiraAPI_CreateFieldsConfig (line 804) | func TestJiraAPI_CreateFieldsConfig(t *testing.T) {
function TestJiraAPI_CreateMetaIssueType (line 948) | func TestJiraAPI_CreateMetaIssueType(t *testing.T) {
function TestJiraAPI_Init (line 982) | func TestJiraAPI_Init(t *testing.T) {
function buildHttpHandler (line 1023) | func buildHttpHandler(successResponse interface{}, errorResponse string)...
FILE: actions/kubernetes.go
constant regoInputPrefix (line 20) | regoInputPrefix = "event.input"
constant KubernetesLabelKey (line 22) | KubernetesLabelKey = "labels"
constant KubernetesAnnotationKey (line 23) | KubernetesAnnotationKey = "annotations"
function IsK8s (line 26) | func IsK8s() bool {
function updateMap (line 31) | func updateMap(old map[string]string, new map[string]string) map[string]...
type KubernetesClient (line 42) | type KubernetesClient struct
method GetName (line 52) | func (k KubernetesClient) GetName() string {
method Init (line 56) | func (k *KubernetesClient) Init() error {
method prepareInputs (line 82) | func (k KubernetesClient) prepareInputs(input map[string]string) (stri...
method Send (line 110) | func (k KubernetesClient) Send(m map[string]string) error {
method Terminate (line 168) | func (k KubernetesClient) Terminate() error {
method GetLayoutProvider (line 173) | func (k KubernetesClient) GetLayoutProvider() layout.LayoutProvider {
function jsonOrString (line 72) | func jsonOrString(input map[string]string, filter string) string {
FILE: actions/kubernetes_test.go
function TestKubernetesClientSend_Labels (line 18) | func TestKubernetesClientSend_Labels(t *testing.T) {
function TestKubernetesClientSend_Annotations (line 140) | func TestKubernetesClientSend_Annotations(t *testing.T) {
FILE: actions/message.go
constant posteeDocsUrl (line 12) | posteeDocsUrl = "https://aquasecurity.github.io/postee/settings/"
function buildShortMessage (line 14) | func buildShortMessage(server, urls string, provider layout.LayoutProvid...
FILE: actions/message_test.go
function Test_buildShortMessage (line 12) | func Test_buildShortMessage(t *testing.T) {
FILE: actions/nexusiq.go
function sanitizedAppName (line 21) | func sanitizedAppName(appName string) string {
type NexusIqAction (line 25) | type NexusIqAction struct
method GetName (line 33) | func (nexus *NexusIqAction) GetName() string {
method Init (line 37) | func (nexus *NexusIqAction) Init() error {
method auth (line 41) | func (nexus *NexusIqAction) auth() string {
method execute (line 45) | func (nexus *NexusIqAction) execute(method string, url string, payload...
method getAppByNameAndOrg (line 94) | func (nexus *NexusIqAction) getAppByNameAndOrg(organizationId string, ...
method createApp (line 110) | func (nexus *NexusIqAction) createApp(organizationId string, appName s...
method createOrGetApp (line 134) | func (nexus *NexusIqAction) createOrGetApp(appName string) (string, er...
method registerBom (line 147) | func (nexus *NexusIqAction) registerBom(appId string, bom string) error {
method Send (line 158) | func (nexus *NexusIqAction) Send(content map[string]string) error {
method Terminate (line 175) | func (nexus *NexusIqAction) Terminate() error {
method GetLayoutProvider (line 180) | func (nexus *NexusIqAction) GetLayoutProvider() layout.LayoutProvider {
FILE: actions/nexusiq_test.go
constant createdAppId (line 16) | createdAppId = "cd8fd2f4f289445b8975092e7d3045ba"
function TestSanitizedAppName (line 18) | func TestSanitizedAppName(t *testing.T) {
function TestNexusiq_Init (line 45) | func TestNexusiq_Init(t *testing.T) {
function TestNexusiq_GetName (line 50) | func TestNexusiq_GetName(t *testing.T) {
function TestNexusiq_Send (line 56) | func TestNexusiq_Send(t *testing.T) {
function configureHttp (line 112) | func configureHttp(t *testing.T, applicationsJson, expctdCreateAppPld, e...
FILE: actions/opsgenie.go
constant defaultPriority (line 17) | defaultPriority = alert.P3
type OpsGenieAction (line 19) | type OpsGenieAction struct
method GetName (line 34) | func (ops *OpsGenieAction) GetName() string {
method Init (line 38) | func (ops *OpsGenieAction) Init() (err error) {
method convertResultToOpsGenie (line 68) | func (ops *OpsGenieAction) convertResultToOpsGenie(title string, conte...
method Send (line 110) | func (ops *OpsGenieAction) Send(input map[string]string) error {
method Terminate (line 127) | func (*OpsGenieAction) Terminate() error {
method GetLayoutProvider (line 132) | func (ops *OpsGenieAction) GetLayoutProvider() layout.LayoutProvider {
function getUserResponders (line 56) | func getUserResponders(users []string) []alert.Responder {
FILE: actions/opsgenie_test.go
function TestGetUserResponders (line 10) | func TestGetUserResponders(t *testing.T) {
function TestConvertResultToOpsGenie (line 37) | func TestConvertResultToOpsGenie(t *testing.T) {
FILE: actions/pagerduty.go
type Clock (line 14) | type Clock interface
type realClock (line 18) | type realClock struct
method Now (line 20) | func (rc *realClock) Now() time.Time {
type PagerdutyClient (line 24) | type PagerdutyClient struct
method GetName (line 33) | func (p *PagerdutyClient) GetName() string {
method Init (line 37) | func (p *PagerdutyClient) Init() error {
method Send (line 50) | func (p *PagerdutyClient) Send(m map[string]string) error {
method Terminate (line 71) | func (p *PagerdutyClient) Terminate() error {
method GetLayoutProvider (line 75) | func (p *PagerdutyClient) GetLayoutProvider() layout.LayoutProvider {
FILE: actions/pagerduty_test.go
type fakeClock (line 17) | type fakeClock struct
method Now (line 19) | func (fc *fakeClock) Now() time.Time {
function TestPagerdutyClient_Init (line 24) | func TestPagerdutyClient_Init(t *testing.T) {
function TestPagerdutyClient_Send (line 48) | func TestPagerdutyClient_Send(t *testing.T) {
FILE: actions/plugin.go
constant ApplicationScopeOwner (line 12) | ApplicationScopeOwner = "<%application_scope_owner%>"
type Action (line 15) | type Action interface
function getHandledRecipients (line 23) | func getHandledRecipients(recipients []string, content *map[string]strin...
function getAppScopeOwners (line 40) | func getAppScopeOwners(content *map[string]string) ([]string, error) {
FILE: actions/servicenow.go
type ServiceNowAction (line 14) | type ServiceNowAction struct
method GetName (line 23) | func (sn *ServiceNowAction) GetName() string {
method Init (line 27) | func (sn *ServiceNowAction) Init() error {
method Send (line 34) | func (sn *ServiceNowAction) Send(content map[string]string) error {
method Terminate (line 75) | func (sn *ServiceNowAction) Terminate() error {
method GetLayoutProvider (line 80) | func (sn *ServiceNowAction) GetLayoutProvider() layout.LayoutProvider {
FILE: actions/slack.go
constant slackBlockLimit (line 18) | slackBlockLimit = 49
type SlackAction (line 21) | type SlackAction struct
method GetName (line 28) | func (slack *SlackAction) GetName() string {
method Init (line 32) | func (slack *SlackAction) Init() error {
method Send (line 57) | func (slack *SlackAction) Send(input map[string]string) error {
method Terminate (line 112) | func (slack *SlackAction) Terminate() error {
method GetLayoutProvider (line 117) | func (slack *SlackAction) GetLayoutProvider() layout.LayoutProvider {
function clearSlackText (line 38) | func clearSlackText(text string) string {
function buildSlackBlock (line 45) | func buildSlackBlock(title string, data []byte) []byte {
FILE: actions/splunk.go
constant defaultSizeLimit (line 21) | defaultSizeLimit = 10000
type SplunkAction (line 23) | type SplunkAction struct
method GetName (line 32) | func (splunk *SplunkAction) GetName() string {
method Init (line 36) | func (splunk *SplunkAction) Init() error {
method Send (line 42) | func (splunk *SplunkAction) Send(d map[string]string) error {
method Terminate (line 144) | func (splunk *SplunkAction) Terminate() error {
method GetLayoutProvider (line 149) | func (splunk *SplunkAction) GetLayoutProvider() layout.LayoutProvider {
FILE: actions/stdout.go
type StdoutAction (line 11) | type StdoutAction struct
method GetName (line 15) | func (stdout StdoutAction) GetName() string { return stdout.Name }
method Init (line 16) | func (stdout StdoutAction) Init() error {
method Send (line 19) | func (stdout StdoutAction) Send(data map[string]string) error {
method Terminate (line 23) | func (stdout StdoutAction) Terminate() error {
method GetLayoutProvider (line 26) | func (stdout StdoutAction) GetLayoutProvider() layout.LayoutProvider {
FILE: actions/teams.go
constant teamsSizeLimit (line 15) | teamsSizeLimit = 18000
type TeamsAction (line 18) | type TeamsAction struct
method GetName (line 25) | func (teams *TeamsAction) GetName() string {
method Init (line 29) | func (teams *TeamsAction) Init() error {
method Send (line 35) | func (teams *TeamsAction) Send(input map[string]string) error {
method Terminate (line 70) | func (teams *TeamsAction) Terminate() error {
method GetLayoutProvider (line 75) | func (teams *TeamsAction) GetLayoutProvider() layout.LayoutProvider {
function escapeJSON (line 79) | func escapeJSON(s string) (string, error) {
FILE: actions/webhook.go
type WebhookAction (line 15) | type WebhookAction struct
method GetName (line 21) | func (webhook *WebhookAction) GetName() string {
method Init (line 25) | func (webhook *WebhookAction) Init() error {
method Send (line 31) | func (webhook *WebhookAction) Send(content map[string]string) error {
method Terminate (line 59) | func (webhook *WebhookAction) Terminate() error {
method GetLayoutProvider (line 64) | func (webhook *WebhookAction) GetLayoutProvider() layout.LayoutProvider {
FILE: actions/webhook_test.go
function TestWebhook_GetName (line 14) | func TestWebhook_GetName(t *testing.T) {
function TestWebhook_Send (line 20) | func TestWebhook_Send(t *testing.T) {
function TestNewClient (line 107) | func TestNewClient(t *testing.T) {
FILE: controller/controller.go
constant NATSEventSubject (line 20) | NATSEventSubject = "postee.events"
constant NATSConfigSubject (line 21) | NATSConfigSubject = "postee.config"
type Controller (line 24) | type Controller struct
method Setup (line 33) | func (c Controller) Setup(r *router.Router) error {
FILE: data/inpteval.go
type Inpteval (line 3) | type Inpteval interface
FILE: data/slack.go
type SlackTextBlock (line 3) | type SlackTextBlock struct
type SlackBlock (line 8) | type SlackBlock struct
FILE: data/types.go
type ScanImageInfo (line 3) | type ScanImageInfo struct
type SensitiveData (line 17) | type SensitiveData struct
type MalwareData (line 24) | type MalwareData struct
type ImageAssuranceResults (line 30) | type ImageAssuranceResults struct
type ControlCheck (line 35) | type ControlCheck struct
type ScanOptions (line 41) | type ScanOptions struct
type VulnerabilitySummary (line 46) | type VulnerabilitySummary struct
type InfoResources (line 57) | type InfoResources struct
type ResourceDetails (line 61) | type ResourceDetails struct
type Vulnerability (line 66) | type Vulnerability struct
FILE: data/utils.go
function ClearField (line 7) | func ClearField(source string) string {
FILE: data/utils_test.go
function TestClearField (line 7) | func TestClearField(t *testing.T) {
FILE: dbservice/actions.go
function MayBeStoreMessage (line 9) | func MayBeStoreMessage(message []byte, messageKey string, expired *time....
FILE: dbservice/changedbpath_test.go
function TestChangeDbPath (line 5) | func TestChangeDbPath(t *testing.T) {
FILE: dbservice/checker.go
function CheckSizeLimit (line 12) | func CheckSizeLimit() {
function CheckExpiredData (line 54) | func CheckExpiredData() {
function getExpired (line 76) | func getExpired(db *bolt.DB) (keys [][]byte, err error) {
FILE: dbservice/checker_test.go
function TestExpiredDates (line 12) | func TestExpiredDates(t *testing.T) {
function TestCheckSizeLimit (line 55) | func TestCheckSizeLimit(t *testing.T) {
function TestWrongBuckets (line 133) | func TestWrongBuckets(t *testing.T) {
function TestDbDelete (line 168) | func TestDbDelete(t *testing.T) {
function TestWithoutAccessToDb (line 210) | func TestWithoutAccessToDb(t *testing.T) {
function dbBucketExists (line 227) | func dbBucketExists(db *bolt.DB, bucket string) (bool, error) {
FILE: dbservice/dbaggregator.go
function AggregateScans (line 9) | func AggregateScans(output string,
FILE: dbservice/dbaggregator_test.go
function TestAggregateScans (line 8) | func TestAggregateScans(t *testing.T) {
FILE: dbservice/dbparam.go
function ChangeDbPath (line 26) | func ChangeDbPath(newPath string) {
function SetNewDbPathFromEnv (line 32) | func SetNewDbPathFromEnv() {
FILE: dbservice/dbparam_test.go
function TestSetNewDbPathFromEnv (line 10) | func TestSetNewDbPathFromEnv(t *testing.T) {
FILE: dbservice/dbservice_test.go
function TestStoreMessage (line 78) | func TestStoreMessage(t *testing.T) {
function TestInitError (line 114) | func TestInitError(t *testing.T) {
function TestSelectError (line 141) | func TestSelectError(t *testing.T) {
function TestInsertError (line 168) | func TestInsertError(t *testing.T) {
function testBucketInsert (line 180) | func testBucketInsert(t *testing.T, testBucket string) {
FILE: dbservice/delete.go
function dbDelete (line 5) | func dbDelete(db *bolt.DB, bucket string, keys [][]byte) error {
FILE: dbservice/invalidinit_test.go
function TestInvalidDbPath (line 52) | func TestInvalidDbPath(t *testing.T) {
function TestBucketInitialization (line 69) | func TestBucketInitialization(t *testing.T) {
FILE: dbservice/plgnstats.go
function RegisterPlgnInvctn (line 9) | func RegisterPlgnInvctn(name string) error {
FILE: dbservice/plgnstats_test.go
function TestRegisterPlgnInvctn (line 11) | func TestRegisterPlgnInvctn(t *testing.T) {
function getPlgnStats (line 36) | func getPlgnStats() (r map[string]int, err error) {
FILE: dbservice/sharedcfg.go
constant apiKeyName (line 14) | apiKeyName = "POSTEE_API_KEY"
function getDbPath (line 17) | func getDbPath() string {
function EnsureApiKey (line 27) | func EnsureApiKey() error {
function GetApiKey (line 51) | func GetApiKey() (string, error) {
function generateApiKey (line 76) | func generateApiKey(length int) (string, error) {
FILE: dbservice/sharedcfg_test.go
function TestApiKey (line 8) | func TestApiKey(t *testing.T) {
function TestApiKeyWithoutInit (line 27) | func TestApiKeyWithoutInit(t *testing.T) {
function TestApiKeyRenewal (line 42) | func TestApiKeyRenewal(t *testing.T) {
FILE: formatting/eval.go
type legacyScnEvaluator (line 15) | type legacyScnEvaluator struct
method Eval (line 19) | func (legacyScnEvaluator *legacyScnEvaluator) Eval(in map[string]inter...
method IsAggregationSupported (line 33) | func (legacyScnEvaluator *legacyScnEvaluator) IsAggregationSupported()...
method BuildAggregatedContent (line 37) | func (legacyScnEvaluator *legacyScnEvaluator) BuildAggregatedContent(s...
function toScanImage (line 66) | func toScanImage(in map[string]interface{}) (*data.ScanImageInfo, error) {
function BuildLegacyScnEvaluator (line 81) | func BuildLegacyScnEvaluator(layoutType string) (data.Inpteval, error) {
FILE: formatting/eval_test.go
function TestEval (line 22) | func TestEval(t *testing.T) {
function TestAggregationSupport (line 58) | func TestAggregationSupport(t *testing.T) {
function TestBuildAggregatedContent (line 65) | func TestBuildAggregatedContent(t *testing.T) {
function TestBuildLegacyScnEvaluator (line 128) | func TestBuildLegacyScnEvaluator(t *testing.T) {
function TestToScanImage (line 159) | func TestToScanImage(t *testing.T) {
FILE: formatting/htmlprovider.go
type HtmlProvider (line 9) | type HtmlProvider struct
method P (line 11) | func (html *HtmlProvider) P(p string) string {
method TitleH1 (line 15) | func (html *HtmlProvider) TitleH1(title string) string {
method TitleH2 (line 19) | func (html *HtmlProvider) TitleH2(title string) string {
method TitleH3 (line 23) | func (html *HtmlProvider) TitleH3(title string) string {
method ColourText (line 27) | func (html *HtmlProvider) ColourText(text, color string) string {
method Table (line 31) | func (html *HtmlProvider) Table(rows [][]string) string {
method A (line 54) | func (html *HtmlProvider) A(url, title string) string {
FILE: formatting/htmlprovider_test.go
function TestHtmlProvider_Table (line 7) | func TestHtmlProvider_Table(t *testing.T) {
function TestHtmlProviderTags (line 28) | func TestHtmlProviderTags(t *testing.T) {
FILE: formatting/jiraprovider.go
type JiraLayoutProvider (line 9) | type JiraLayoutProvider struct
method P (line 11) | func (jira *JiraLayoutProvider) P(p string) string {
method TitleH1 (line 15) | func (jira *JiraLayoutProvider) TitleH1(title string) string {
method TitleH2 (line 19) | func (jira *JiraLayoutProvider) TitleH2(title string) string {
method TitleH3 (line 23) | func (jira *JiraLayoutProvider) TitleH3(title string) string {
method ColourText (line 27) | func (jira *JiraLayoutProvider) ColourText(text, color string) string {
method Table (line 31) | func (jira *JiraLayoutProvider) Table(rows [][]string) string {
method A (line 47) | func (jira *JiraLayoutProvider) A(url, title string) string {
FILE: formatting/jiraprovider_test.go
function TestJiraLayoutProvider_Tags (line 5) | func TestJiraLayoutProvider_Tags(t *testing.T) {
function TestJiraLayoutProvider_Table (line 22) | func TestJiraLayoutProvider_Table(t *testing.T) {
FILE: formatting/markup_test.go
type tagsTest (line 9) | type tagsTest struct
type tableTest (line 16) | type tableTest struct
function tagsTesting (line 21) | func tagsTesting(tests []tagsTest, t *testing.T, provider layout.LayoutP...
function tableTesting (line 45) | func tableTesting(tests []tableTest, t *testing.T, provider layout.Layou...
FILE: formatting/slackmrkdwnprovider.go
function getMrkdwnText (line 12) | func getMrkdwnText(text string) string {
type SlackMrkdwnProvider (line 29) | type SlackMrkdwnProvider struct
method TitleH1 (line 31) | func (mrkdwn *SlackMrkdwnProvider) TitleH1(title string) string {
method TitleH2 (line 35) | func (mrkdwn *SlackMrkdwnProvider) TitleH2(title string) string {
method TitleH3 (line 39) | func (mrkdwn *SlackMrkdwnProvider) TitleH3(title string) string {
method ColourText (line 43) | func (mrkdwn *SlackMrkdwnProvider) ColourText(text, color string) stri...
method Table (line 47) | func (mrkdwn *SlackMrkdwnProvider) Table(rows [][]string) string {
method P (line 139) | func (mrkdwn *SlackMrkdwnProvider) P(p string) string {
method A (line 143) | func (mrkdwn *SlackMrkdwnProvider) A(url, title string) string {
FILE: formatting/slackmrkdwnprovider_test.go
function TestSlackMrkdwn (line 5) | func TestSlackMrkdwn(t *testing.T) {
function TestSlackMrkdwnProvider_Table (line 22) | func TestSlackMrkdwnProvider_Table(t *testing.T) {
FILE: integration/controller_runner_test.go
constant RunnerConfig (line 18) | RunnerConfig = `
function TestControllerRunner_Happy (line 38) | func TestControllerRunner_Happy(t *testing.T) {
FILE: layout/assurances.go
function RenderAssurances (line 9) | func RenderAssurances(provider LayoutProvider, assuranceResults data.Ima...
FILE: layout/colors.go
function CriticalColor (line 3) | func CriticalColor() string { return "#c00000" }
function HighColor (line 4) | func HighColor() string { return "#e0443d" }
function MediumColor (line 5) | func MediumColor() string { return "#f79421" }
function LowColor (line 6) | func LowColor() string { return "#e1c930" }
function NegligibleColor (line 7) | func NegligibleColor() string { return "green" }
FILE: layout/malware.go
function RenderMalware (line 10) | func RenderMalware(malware []data.MalwareData, provider LayoutProvider, ...
FILE: layout/provider.go
type LayoutProvider (line 3) | type LayoutProvider interface
FILE: layout/sensitive.go
function RenderSensitiveData (line 9) | func RenderSensitiveData(sensitive []data.SensitiveData, provider Layout...
FILE: layout/ticketLayout.go
function GenTestDescription (line 10) | func GenTestDescription(provider LayoutProvider, raw string) string {
function GenTicketDescription (line 16) | func GenTicketDescription(provider LayoutProvider, scanInfo, prevScan *d...
FILE: layout/vulnerabilities.go
constant empty (line 10) | empty = "none"
function RenderVulnerabilities (line 12) | func RenderVulnerabilities(resources []data.InfoResources, provider Layo...
function VulnerabilitiesTable (line 56) | func VulnerabilitiesTable(provider LayoutProvider, rows [2][]string) str...
FILE: main.go
constant URL (line 23) | URL = "0.0.0.0:8082"
constant TLS (line 24) | TLS = "0.0.0.0:8445"
constant URL_USAGE (line 25) | URL_USAGE = "The socket to bind to, specified using host:port."
constant TLS_USAGE (line 26) | TLS_USAGE = "The TLS socket to bind to, specified using host:port."
constant CFG_FILE (line 27) | CFG_FILE = "/config/cfg.yaml"
constant CFG_USAGE (line 28) | CFG_USAGE = "The alert configuration file."
function init (line 56) | func init() {
function main (line 75) | func main() {
function Daemonize (line 174) | func Daemonize() {
FILE: msgservice/aggregatebytime_test.go
function TestAggregateByTimeout (line 14) | func TestAggregateByTimeout(t *testing.T) {
FILE: msgservice/aggregatescan_test.go
function TestAggregateIssuesPerTicket (line 12) | func TestAggregateIssuesPerTicket(t *testing.T) {
function doAggregate (line 47) | func doAggregate(t *testing.T, caseDesc string, expectedSntCnt int, expe...
FILE: msgservice/applicationscopeowner_test.go
function TestApplicationScopeOwner (line 23) | func TestApplicationScopeOwner(t *testing.T) {
FILE: msgservice/calculateexpired_test.go
function TestCalculateExpired (line 10) | func TestCalculateExpired(t *testing.T) {
FILE: msgservice/getuniqueid_test.go
function TestScanUniqueId (line 47) | func TestScanUniqueId(t *testing.T) {
function sendInputs (line 90) | func sendInputs(t *testing.T, caseDesc string, inputs []string, uniqueMe...
function TestGetMessageUniqueId (line 128) | func TestGetMessageUniqueId(t *testing.T) {
FILE: msgservice/logs.go
function prnInputLogs (line 5) | func prnInputLogs(msg string, v ...interface{}) {
FILE: msgservice/msghandling.go
type MsgService (line 16) | type MsgService struct
method MsgHandling (line 19) | func (scan *MsgService) MsgHandling(input []byte, output actions.Actio...
method EvaluateRegoRule (line 110) | func (scan *MsgService) EvaluateRegoRule(r *routes.InputRoute, input [...
function send (line 137) | func send(otpt actions.Action, cnt map[string]string) {
function calculateExpired (line 150) | func calculateExpired(UniqueMessageTimeoutSeconds int) *time.Time {
FILE: msgservice/msgservice_mocks_test.go
type DemoInptEval (line 19) | type DemoInptEval struct
method Eval (line 27) | func (inptEval *DemoInptEval) Eval(in map[string]interface{}, serverUr...
method BuildAggregatedContent (line 42) | func (inptEval *DemoInptEval) BuildAggregatedContent(items []map[strin...
method IsAggregationSupported (line 59) | func (inptEval *DemoInptEval) IsAggregationSupported() bool {
type DemoEmailAction (line 63) | type DemoEmailAction struct
method GetName (line 70) | func (plg *DemoEmailAction) GetName() string {
method getEmailsCount (line 74) | func (plg *DemoEmailAction) getEmailsCount() int {
method Init (line 81) | func (plg *DemoEmailAction) Init() error { return nil }
method Send (line 82) | func (plg *DemoEmailAction) Send(data map[string]string) error {
method Terminate (line 96) | func (plg *DemoEmailAction) Terminate() error { return nil }
method GetLayoutProvider (line 97) | func (plg *DemoEmailAction) GetLayoutProvider() layout.LayoutProvider {
FILE: msgservice/msgservice_scan_test.go
function getImportantData (line 71) | func getImportantData(scan *data.ScanImageInfo) map[string]string {
function Equal (line 106) | func Equal(A, B *data.ScanImageInfo) bool {
function BenchmarkGenTicketDescription (line 133) | func BenchmarkGenTicketDescription(b *testing.B) {
function TestGenTicketDescription (line 140) | func TestGenTicketDescription(t *testing.T) {
function TestGenTicketDescriptionFieldSeeMore (line 167) | func TestGenTicketDescriptionFieldSeeMore(t *testing.T) {
FILE: msgservice/msgservice_test.go
type FailingInptEval (line 19) | type FailingInptEval struct
method Eval (line 24) | func (inptEval *FailingInptEval) Eval(in map[string]interface{}, serve...
method BuildAggregatedContent (line 34) | func (inptEval *FailingInptEval) BuildAggregatedContent(items []map[st...
method IsAggregationSupported (line 38) | func (inptEval *FailingInptEval) IsAggregationSupported() bool {
function TestInputs (line 42) | func TestInputs(t *testing.T) {
function validateInputValue (line 64) | func validateInputValue(t *testing.T, caseDesc string, input []byte, sho...
function TestEvalError (line 103) | func TestEvalError(t *testing.T) {
function TestAggrEvalError (line 136) | func TestAggrEvalError(t *testing.T) {
function TestEmptyInput (line 172) | func TestEmptyInput(t *testing.T) {
function TestMalformedJSON (line 198) | func TestMalformedJSON(t *testing.T) {
FILE: msgservice/regocriteria_test.go
function TestRegoCriteria (line 32) | func TestRegoCriteria(t *testing.T) {
function validateRegoInput (line 95) | func validateRegoInput(t *testing.T, caseDesc string, input string, rego...
FILE: msgservice/scheduler_test.go
function TestScheduler (line 13) | func TestScheduler(t *testing.T) {
FILE: msgservice/uniquemsgkey.go
constant propSep (line 9) | propSep = "."
function GetMessageUniqueId (line 12) | func GetMessageUniqueId(in map[string]interface{}, props []string) string {
function getSingleValue (line 24) | func getSingleValue(o interface{}, parts []string) string {
FILE: regoservice/aggregation_test.go
function TestAggregation (line 42) | func TestAggregation(t *testing.T) {
function aggregateBuildinRego (line 77) | func aggregateBuildinRego(t *testing.T, regoRule *string, aggregationReg...
FILE: regoservice/eval.go
constant result_prop (line 16) | result_prop = "result"
constant title_prop (line 17) | title_prop = "title"
constant url_prop (line 18) | url_prop = "url"
constant aggregation_pkg_prop (line 19) | aggregation_pkg_prop = "aggregation_pkg"
constant dateProp (line 22) | dateProp = "result_date"
constant severityProp (line 23) | severityProp = "result_severity"
constant categoryProp (line 24) | categoryProp = "result_category"
constant subcategoryProp (line 25) | subcategoryProp = "result_subcategory"
constant assignedToProp (line 26) | assignedToProp = "result_assigned_to"
constant assignedGroupProp (line 27) | assignedGroupProp = "result_assigned_group"
constant summaryProp (line 28) | summaryProp = "result_summary"
type regoEvaluator (line 36) | type regoEvaluator struct
method IsAggregationSupported (line 42) | func (regoEvaluator *regoEvaluator) IsAggregationSupported() bool {
method Eval (line 46) | func (regoEvaluator *regoEvaluator) Eval(in map[string]interface{}, se...
method BuildAggregatedContent (line 166) | func (regoEvaluator *regoEvaluator) BuildAggregatedContent(scans []map...
function getStringFromData (line 110) | func getStringFromData(data map[string]interface{}, prop string) string {
function getFirstElement (line 125) | func getFirstElement(context map[string]interface{}, key string) interfa...
function asStringOrJson (line 144) | func asStringOrJson(data map[string]interface{}, prop string) (string, e...
function BuildBundledRegoEvaluator (line 253) | func BuildBundledRegoEvaluator(rego_package string) (data.Inpteval, erro...
function buildBundledRegoForPackage (line 272) | func buildBundledRegoForPackage(rego_package string) (*rego.PreparedEval...
function filterRegoTemplateFiles (line 291) | func filterRegoTemplateFiles(_ string, info fs.FileInfo, _ int) bool {
function buildAggregatedRego (line 298) | func buildAggregatedRego(query *rego.PreparedEvalQuery) (*rego.PreparedE...
function BuildExternalRegoEvaluator (line 327) | func BuildExternalRegoEvaluator(filename string, body string) (data.Inpt...
FILE: regoservice/eval_test.go
function TestEval (line 15) | func TestEval(t *testing.T) {
function evaluateBuildinRego (line 257) | func evaluateBuildinRego(t *testing.T, inputFile, templateFile, descript...
function evaluateExternalRego (line 302) | func evaluateExternalRego(t *testing.T, inputFile, templateFile, descrip...
function compareDescriptions (line 352) | func compareDescriptions(t *testing.T, expectedFile, gotFile string) {
function TestBuildBundledRegoForPackage (line 361) | func TestBuildBundledRegoForPackage(t *testing.T) {
FILE: regoservice/jsonformat.go
function jsonFmtFunc (line 12) | func jsonFmtFunc() func(r *rego.Rego) {
FILE: regoservice/regocheck.go
constant module (line 14) | module = `package postee
constant defaultPathToRegoFilters (line 22) | defaultPathToRegoFilters = "./rego-filters"
function getFilesWithPathToRegoFilters (line 27) | func getFilesWithPathToRegoFilters(files []string) []string {
function buildRegoLoader (line 45) | func buildRegoLoader(files []string, rule string) func(r *rego.Rego) {
function IsUsedRegoFiles (line 53) | func IsUsedRegoFiles(files []string) bool {
function DoesMatchRegoCriteria (line 56) | func DoesMatchRegoCriteria(input interface{}, files []string, rule strin...
FILE: regoservice/regocheck_test.go
function TestOpaRego (line 10) | func TestOpaRego(t *testing.T) {
function TestGetFilesWithPathToRegoFilters (line 78) | func TestGetFilesWithPathToRegoFilters(t *testing.T) {
FILE: router/anonymizeSettings_test.go
function TestAnonymizeSettings (line 5) | func TestAnonymizeSettings(t *testing.T) {
FILE: router/anonymizer.go
function anonymizeSettings (line 5) | func anonymizeSettings(settings *ActionSettings) *ActionSettings {
FILE: router/builders.go
function buildStdoutAction (line 13) | func buildStdoutAction(sourceSettings *ActionSettings) *actions.StdoutAc...
function buildSplunkAction (line 17) | func buildSplunkAction(sourceSettings *ActionSettings) *actions.SplunkAc...
function buildWebhookAction (line 27) | func buildWebhookAction(sourceSettings *ActionSettings) *actions.Webhook...
function buildTeamsAction (line 35) | func buildTeamsAction(sourceSettings *ActionSettings, aquaServer string)...
function buildServiceNow (line 43) | func buildServiceNow(sourceSettings *ActionSettings) *actions.ServiceNow...
function buildSlackAction (line 57) | func buildSlackAction(sourceSettings *ActionSettings, aqua string) *acti...
function buildEmailAction (line 65) | func buildEmailAction(sourceSettings *ActionSettings) *actions.EmailActi...
function buildNexusIqAction (line 78) | func buildNexusIqAction(sourceSettings *ActionSettings) *actions.NexusIq...
function buildDependencyTrackAction (line 88) | func buildDependencyTrackAction(sourceSettings *ActionSettings) *actions...
function buildOpsGenieAction (line 96) | func buildOpsGenieAction(sourceSettings *ActionSettings) *actions.OpsGen...
function buildJiraAction (line 110) | func buildJiraAction(sourceSettings *ActionSettings) *actions.JiraAPI {
function buildExecAction (line 137) | func buildExecAction(sourceSettings *ActionSettings) (*actions.ExecClien...
function buildHTTPAction (line 162) | func buildHTTPAction(sourceSettings *ActionSettings) (*actions.HTTPClien...
function buildKubernetesAction (line 205) | func buildKubernetesAction(sourceSettings *ActionSettings) (*actions.Kub...
function buildDockerAction (line 225) | func buildDockerAction(sourceSettings *ActionSettings) (*actions.DockerC...
function buildAWSSecurityHubAction (line 240) | func buildAWSSecurityHubAction(sourceSettings *ActionSettings) (*actions...
function buildPagerdutyAction (line 244) | func buildPagerdutyAction(sourceSettings *ActionSettings) (*actions.Page...
FILE: router/initoutputs_test.go
function TestBuildAndInitOtpt (line 11) | func TestBuildAndInitOtpt(t *testing.T) {
FILE: router/inittemplate_test.go
function TestInitTemplate (line 17) | func TestInitTemplate(t *testing.T) {
function doInitTemplate (line 107) | func doInitTemplate(t *testing.T, caseDesc string, template *Template, e...
function getMockedHttpClient (line 132) | func getMockedHttpClient() *http.Client {
type RoundTripFunc (line 137) | type RoundTripFunc
method RoundTrip (line 140) | func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, e...
function NewTestClient (line 145) | func NewTestClient(fn RoundTripFunc) *http.Client {
function responseWithRego (line 150) | func responseWithRego(req *http.Request) (*http.Response, error) {
function newTestResponse (line 162) | func newTestResponse(status int, response string) *http.Response {
FILE: router/integrations.go
type ActionSettings (line 3) | type ActionSettings struct
FILE: router/loads_test.go
type ctxWrapper (line 18) | type ctxWrapper struct
method MsgHandling (line 35) | func (ctx *ctxWrapper) MsgHandling(input []byte, action actions.Action...
method setup (line 44) | func (ctxWrapper *ctxWrapper) setup(cfg string) {
method teardown (line 75) | func (ctxWrapper *ctxWrapper) teardown() {
method EvaluateRegoRule (line 89) | func (ctx *ctxWrapper) EvaluateRegoRule(r *routes.InputRoute, _ []byte...
type invctn (line 29) | type invctn struct
function TestLoads (line 96) | func TestLoads(t *testing.T) {
function TestReload (line 175) | func TestReload(t *testing.T) {
function TestServiceGetters (line 266) | func TestServiceGetters(t *testing.T) {
FILE: router/parsecfg.go
constant v1Marker (line 12) | v1Marker = "- type: common"
constant v1Warning (line 13) | v1Warning = `
function Parsev2cfg (line 27) | func Parsev2cfg(cfgpath string) (*TenantSettings, error) {
function checkV1Cfg (line 47) | func checkV1Cfg(data []byte, cfgpath string) {
FILE: router/parsecfg_test.go
function TestParseCfgWithInvalidFilename (line 9) | func TestParseCfgWithInvalidFilename(t *testing.T) {
function TestParseCfgWithInvalidYaml (line 18) | func TestParseCfgWithInvalidYaml(t *testing.T) {
FILE: router/routehandling_test.go
function TestHandling (line 203) | func TestHandling(t *testing.T) {
function runTestRouteHandlingCase (line 272) | func runTestRouteHandlingCase(t *testing.T, caseDesc string, cfg string,...
function TestInvalidRouteName (line 323) | func TestInvalidRouteName(t *testing.T) {
function TestRouteWithNoValidRego (line 352) | func TestRouteWithNoValidRego(t *testing.T) {
function TestSend (line 381) | func TestSend(t *testing.T) {
FILE: router/router.go
constant ServiceNowTableDefault (line 30) | ServiceNowTableDefault = "incident"
constant AnonymizeReplacement (line 31) | AnonymizeReplacement = "<hidden>"
type Router (line 34) | type Router struct
method ReloadConfig (line 81) | func (ctx *Router) ReloadConfig() {
method Start (line 90) | func (ctx *Router) Start(cfgfile string) error {
method Terminate (line 107) | func (ctx *Router) Terminate() {
method Send (line 138) | func (ctx *Router) Send(data []byte) {
method GetCurrentEvents (line 144) | func (ctx *Router) GetCurrentEvents() []any {
method initTemplate (line 152) | func (ctx *Router) initTemplate(template *Template) error {
method load (line 213) | func (ctx *Router) load() error {
method HandleRoute (line 285) | func (ctx *Router) HandleRoute(routeName string, in []byte) {
method handle (line 352) | func (ctx *Router) handle(in []byte) {
method listen (line 459) | func (ctx *Router) listen() {
function Instance (line 66) | func Instance() *Router {
type service (line 272) | type service interface
function BuildAndInitOtpt (line 357) | func BuildAndInitOtpt(settings *ActionSettings, aquaServerUrl string) ac...
function buildRunnerConfig (line 484) | func buildRunnerConfig(runnerName, cfgFile string) (string, error) {
function contains (line 536) | func contains(haystack []Template, needle string) bool {
function SetupConnOptions (line 545) | func SetupConnOptions(opts []nats.Option) []nats.Option {
FILE: router/router_test.go
function Test_buildRunnerConfig (line 9) | func Test_buildRunnerConfig(t *testing.T) {
FILE: router/rule.go
type Rule (line 3) | type Rule struct
FILE: router/sizeparser.go
constant B (line 11) | B = 1
constant KB (line 12) | KB = 1024
constant MB (line 13) | MB = 1024 * KB
constant GB (line 14) | GB = 1024 * MB
function parseSize (line 24) | func parseSize(sizeStr string) int {
FILE: router/sizeparser_test.go
function TestParseSize (line 10) | func TestParseSize(t *testing.T) {
FILE: router/template.go
type Template (line 3) | type Template struct
FILE: router/tenants.go
type TenantSettings (line 7) | type TenantSettings struct
FILE: routes/aggrtimeout.go
function parseTimeouts (line 9) | func parseTimeouts(v string) (int, error) {
function ConfigureTimeouts (line 41) | func ConfigureTimeouts(route *InputRoute) *InputRoute {
FILE: routes/aggrtimeout_test.go
function TestTimeouts (line 59) | func TestTimeouts(t *testing.T) {
FILE: routes/routes.go
type InputRoute (line 3) | type InputRoute struct
method IsSchedulerRun (line 23) | func (route *InputRoute) IsSchedulerRun() bool {
method StartScheduler (line 26) | func (route *InputRoute) StartScheduler() {
method StopScheduler (line 30) | func (route *InputRoute) StopScheduler() {
type Plugins (line 14) | type Plugins struct
FILE: routes/routes_test.go
function TestScheduling (line 7) | func TestScheduling(t *testing.T) {
FILE: runner/runner.go
constant NATSConfigSubject (line 14) | NATSConfigSubject = "postee.config"
type Runner (line 17) | type Runner struct
method Setup (line 26) | func (r Runner) Setup(rtr *router.Router, cfg *os.File) error {
FILE: servicenow/insert_table.go
function InsertRecordToTable (line 12) | func InsertRecordToTable(user, password, instance, table string, content...
FILE: servicenow/servicenow_base.go
constant BaseServer (line 4) | BaseServer = "service-now.com/"
constant baseApiUrl (line 5) | baseApiUrl = "api/now/"
constant tableApi (line 6) | tableApi = "table/"
type ServiceNowData (line 9) | type ServiceNowData struct
FILE: slack/sendtoslack.go
function SendToUrl (line 11) | func SendToUrl(url string, data []byte) error {
FILE: teams/teams_requests.go
function CreateMessageByWebhook (line 12) | func CreateMessageByWebhook(webhook, content string) error {
FILE: ui/backend/dbservice/getplgnstats.go
function GetPlgnStats (line 11) | func GetPlgnStats() (r map[string]int, err error) {
FILE: ui/backend/main.go
constant ENV_FILELOG (line 11) | ENV_FILELOG = "POSTEE_UI_LOGFILE"
constant ENV_CFG (line 12) | ENV_CFG = "POSTEE_UI_CFG"
constant ENV_WEB (line 13) | ENV_WEB = "POSTEE_UI_WEB"
constant ENV_UPDATE_URL (line 14) | ENV_UPDATE_URL = "POSTEE_UI_UPDATE_URL"
constant ENV_PORT (line 15) | ENV_PORT = "POSTEE_UI_PORT"
constant ENV_ADMIN_USER (line 16) | ENV_ADMIN_USER = "POSTEE_ADMIN_USER"
constant ENV_ADMIN_PASSWORD (line 17) | ENV_ADMIN_PASSWORD = "POSTEE_ADMIN_PASSWORD"
constant DEFAULT_WEB_PATH (line 19) | DEFAULT_WEB_PATH = "/uiserver/www"
function main (line 22) | func main() {
FILE: ui/backend/uiserver/authentication.go
constant sessioncookiename (line 8) | sessioncookiename = "postee-session-cookie"
method login (line 11) | func (srv *uiServer) login(w http.ResponseWriter, r *http.Request) {
method logout (line 37) | func (srv *uiServer) logout(w http.ResponseWriter, r *http.Request) {
FILE: ui/backend/uiserver/authentication_middleware.go
method authenticationMiddleware (line 8) | func (srv *uiServer) authenticationMiddleware(next http.Handler) http.Ha...
method getUserFromRequest (line 30) | func (srv *uiServer) getUserFromRequest(r *http.Request) (string, error) {
FILE: ui/backend/uiserver/config.go
method getConfig (line 15) | func (srv *uiServer) getConfig(w http.ResponseWriter, r *http.Request) {
method updateConfig (line 38) | func (srv *uiServer) updateConfig(w http.ResponseWriter, r *http.Request) {
function reloadWebhookCfg (line 82) | func reloadWebhookCfg(url string, key string) error {
FILE: ui/backend/uiserver/events.go
method getEvents (line 10) | func (srv *uiServer) getEvents(w http.ResponseWriter, r *http.Request) {
FILE: ui/backend/uiserver/events_test.go
function TestUiServer_getEvents (line 15) | func TestUiServer_getEvents(t *testing.T) {
FILE: ui/backend/uiserver/httpserver.go
type localWebServer (line 9) | type localWebServer struct
method ServeHTTP (line 14) | func (web *localWebServer) ServeHTTP(w http.ResponseWriter, r *http.Re...
FILE: ui/backend/uiserver/plgnstats.go
method plgnStats (line 10) | func (srv *uiServer) plgnStats(w http.ResponseWriter, r *http.Request) {
function handleErr (line 25) | func handleErr(w http.ResponseWriter, err error) {
FILE: ui/backend/uiserver/server.go
type uiServer (line 12) | type uiServer struct
method Start (line 66) | func (srv *uiServer) Start() {
method Stop (line 71) | func (srv *uiServer) Stop() {
method pingHandler (line 75) | func (ctx *uiServer) pingHandler(w http.ResponseWriter, r *http.Reques...
function Instance (line 24) | func Instance(webLocalPath, port, cfg, webhookUrl, admusr string, admpwd...
FILE: ui/backend/uiserver/testplg.go
method testSettings (line 14) | func (srv *uiServer) testSettings(w http.ResponseWriter, r *http.Request) {
FILE: ui/backend/uiserver/update_test.go
constant testCfgFile (line 13) | testCfgFile = "test.cfg"
constant inputConfigJson (line 15) | inputConfigJson = `[{"type":"common"}]`
function TestUpdateConfig (line 18) | func TestUpdateConfig(t *testing.T) {
FILE: ui/frontend/src/components/form.js
method updateField (line 4) | updateField(e) {
method updateCollectionField (line 24) | updateCollectionField(e) {
method isFormValid (line 30) | isFormValid() {
FILE: ui/frontend/src/components/validator.js
class validator (line 1) | class validator {
method constructor (line 2) | constructor(fields, validationFn) {
method url (line 14) | url(label, value) {
method email (line 30) | email(label, value) {
method required (line 35) | required(label, value) {
method validateJiraPasswordandToken (line 39) | validateJiraPasswordandToken() {
method recipients (line 49) | recipients(label, value) {
method v (line 63) | v(validationFn) {
FILE: ui/frontend/src/store/modules/account.js
method login (line 10) | login(context, payload) {
method logout (line 28) | logout(context) {
method update (line 40) | update(state, info) {
FILE: ui/frontend/src/store/modules/actions.js
function updateActions (line 2) | function updateActions(context, actions) {
method test (line 13) | test(context, settings) {
method update (line 30) | update(context, payload) {
method remove (line 41) | remove(context, name) {
method add (line 46) | add(context, settings) {
method set (line 56) | set(state, actions) {
FILE: ui/frontend/src/store/modules/error.js
method set (line 10) | set(state, error) {
method clear (line 13) | clear(state) {
FILE: ui/frontend/src/store/modules/events.js
method load (line 9) | load(context) {
method set (line 19) | set(state, payload) {
FILE: ui/frontend/src/store/modules/flags.js
method set (line 8) | set(state, flags) {
FILE: ui/frontend/src/store/modules/routes.js
function updateRoutes (line 3) | function updateRoutes(context, routes) {
method update (line 15) | update(context, payload) {
method remove (line 26) | remove(context, name) {
method add (line 31) | add(context, settings) {
method set (line 41) | set(state, routes) {
FILE: ui/frontend/src/store/modules/rules.js
function updateRules (line 3) | function updateRules(context, rules) {
method update (line 15) | update(context, payload) {
method remove (line 26) | remove(context, name) {
method add (line 31) | add(context, settings) {
method set (line 41) | set(state, rules) {
FILE: ui/frontend/src/store/modules/settings.js
method update (line 7) | update(context, payload) {
method set (line 17) | set(state, settings) {
FILE: ui/frontend/src/store/modules/stats.js
method load (line 8) | load(context) {
method set (line 18) | set(state, payload) {
FILE: ui/frontend/src/store/modules/templates.js
function updateTemplates (line 3) | function updateTemplates(context, templates) {
method update (line 15) | update(context, payload) {
method remove (line 26) | remove(context, name) {
method add (line 31) | add(context, settings) {
method set (line 41) | set(state, templates) {
FILE: ui/frontend/src/store/store.js
method getAppState (line 32) | getAppState(state) {
method load (line 37) | load(context) {
FILE: utils/cert.go
function publicKey (line 19) | func publicKey(priv interface{}) interface{} {
function pemBlockForKey (line 30) | func pemBlockForKey(priv interface{}) *pem.Block {
function generateCertificate (line 45) | func generateCertificate(hosts []string, keyFile string, certFile string...
function getHostnames (line 117) | func getHostnames() ([]string, error) {
function GenerateCertificate (line 170) | func GenerateCertificate(keyFile string, certFile string) error {
FILE: utils/prnheaders.go
function PrnLogResponse (line 8) | func PrnLogResponse(body io.ReadCloser) string {
FILE: utils/utils.go
function GetEnvironmentVarOrPlain (line 15) | func GetEnvironmentVarOrPlain(value string) string {
function InitDebug (line 23) | func InitDebug() {
function Debug (line 32) | func Debug(format string, v ...interface{}) {
function GetEnv (line 38) | func GetEnv(name string) (string, error) {
function GetRootDir (line 48) | func GetRootDir() (string, error) {
function PathExists (line 53) | func PathExists(name string) bool {
FILE: webserver/reload.go
method reload (line 9) | func (web *WebServer) reload(w http.ResponseWriter, r *http.Request) {
FILE: webserver/tenant.go
method tenantHandler (line 13) | func (ctx *WebServer) tenantHandler(w http.ResponseWriter, r *http.Reque...
FILE: webserver/webserver.go
type WebServer (line 19) | type WebServer struct
method withApiKey (line 36) | func (ctx *WebServer) withApiKey(next http.HandlerFunc) http.HandlerFu...
method Start (line 55) | func (ctx *WebServer) Start(host, tlshost string) {
method Terminate (line 99) | func (ctx *WebServer) Terminate() {
method sessionHandler (line 104) | func (ctx *WebServer) sessionHandler(f func(http.ResponseWriter, *http...
method scanHandler (line 110) | func (ctx *WebServer) scanHandler(w http.ResponseWriter, r *http.Reque...
method pingHandler (line 124) | func (ctx *WebServer) pingHandler(w http.ResponseWriter, r *http.Reque...
method writeResponse (line 128) | func (ctx *WebServer) writeResponse(w http.ResponseWriter, httpStatus ...
method writeResponseError (line 140) | func (ctx *WebServer) writeResponseError(w http.ResponseWriter, httpEr...
method eventsHandler (line 149) | func (ctx *WebServer) eventsHandler(w http.ResponseWriter, r *http.Req...
function Instance (line 27) | func Instance() *WebServer {
FILE: webserver/webserver_test.go
function TestWebServer_eventsHandler (line 13) | func TestWebServer_eventsHandler(t *testing.T) {
Condensed preview — 335 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,180K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/BUG_REPORT.md",
"chars": 385,
"preview": "---\nname: Bug Report\nlabels: kind/bug\nabout: If something isn't working as expected.\n---\n\n## Description\n\n<!--\nBriefly d"
},
{
"path": ".github/ISSUE_TEMPLATE/FEATURE_REQUEST.md",
"chars": 681,
"preview": "---\nname: Feature Request\nlabels: kind/feature\nabout: I have a suggestion (and might want to implement myself)!\n---\n\n# D"
},
{
"path": ".github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md",
"chars": 229,
"preview": "---\nname: Support Question\nlabels: triage/support\nabout: If you have a question about Postee.\n---\n\n<!--\nIf you have a tr"
},
{
"path": ".github/dependabot.yml",
"chars": 177,
"preview": "version: 2\nupdates:\n- package-ecosystem: github-actions\n directory: /\n schedule:\n interval: daily\n- package-ecosyst"
},
{
"path": ".github/workflows/aqua-cloud.yml",
"chars": 709,
"preview": "name: Aqua Cloud\n\non:\n push:\n branches: [ main ]\n schedule:\n - cron: '15 21 * * 2'\n\njobs:\n build:\n name: Vul"
},
{
"path": ".github/workflows/go.yml",
"chars": 1433,
"preview": "---\nname: Pull Request\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\nenv:\n GO_VERSION: \"1.18\"\n"
},
{
"path": ".github/workflows/publish-chart.yml",
"chars": 2957,
"preview": "# Triggered manually using as input the release e.g. v0.0.1\nname: Publish Helm Chart\non:\n pull_request:\n branches:\n "
},
{
"path": ".github/workflows/publish-docs.yml",
"chars": 1316,
"preview": "---\n# This is a manually triggered workflow to build and publish the MkDocs from the\n# specified Git revision to GitHub "
},
{
"path": ".github/workflows/release.yml",
"chars": 2342,
"preview": "name: Release\non:\n push:\n tags:\n - \"*\"\n workflow_dispatch:\nenv:\n GO_VERSION: \"1.18\"\njobs:\n tests:\n name: "
},
{
"path": ".gitignore",
"chars": 110,
"preview": ".idea/\nbin/\npkg/\nsrc/github.com/\nsrc/gopkg.in/\nsrc/go.etcd.io/\n**/*.out\n**/*.db\ncoverage.txt\ndist/\n.vscode/\n\n\n"
},
{
"path": ".golangci.yml",
"chars": 129,
"preview": "run:\n timeout: 5m\nlinters:\n enable:\n - errorlint\n - govet\n disable:\n - gosimple\n - ineffassign\n - stat"
},
{
"path": ".goreleaser.yml",
"chars": 3805,
"preview": "project_name: postee\nrelease:\n draft: false\n prerelease: auto\nenv:\n - GO111MODULE=on\n - CGO_ENABLED=0\nbefore:\n hook"
},
{
"path": ".yamllint",
"chars": 115,
"preview": "---\nextends: default\n\nrules:\n line-length: disable\n truthy: disable\n document-start: disable\n\nignore: |\n /src/\n"
},
{
"path": "Dockerfile",
"chars": 820,
"preview": "FROM golang:1.18-alpine as builder\n# RUN apk add --update git\nCOPY . /server/\nWORKDIR /server/\nARG TARGETOS TARGETARCH\nR"
},
{
"path": "Dockerfile.release",
"chars": 522,
"preview": "FROM alpine:3.18.2\nRUN apk add --no-cache \\\n ca-certificates \\\n curl \\\n jq \\\n wget\nEXPOSE 8082\nEXPOSE 8445\nRUN mkdir"
},
{
"path": "Dockerfile.ui",
"chars": 821,
"preview": "FROM node:18-alpine3.17 as vuebuilder\nCOPY ./ui/frontend /frontend\nWORKDIR /frontend\n\nRUN yarn install\nRUN yarn build\n\n\n"
},
{
"path": "LICENSE",
"chars": 1057,
"preview": "MIT License\n\nCopyright (c) 2016 \n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this s"
},
{
"path": "Makefile",
"chars": 1334,
"preview": "# Set the default goal\n.DEFAULT_GOAL := build\nVERSION := $(shell git describe --tags)\nLDFLAGS=-ldflags \"-s -w -X=main.ve"
},
{
"path": "README.md",
"chars": 3229,
"preview": "# Notice: Postee is no longer under active development or maintenance.\n\n<p align=\"center\">\n <img src=\"./docs/img/postee"
},
{
"path": "actions/aws_securityhub.go",
"chars": 5939,
"preview": "package actions\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/form"
},
{
"path": "actions/aws_securityhub_test.go",
"chars": 8152,
"preview": "package actions\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/aws/aws-sdk-go-v2/aws\"\n\t\"github.com/aws/aws-sdk-go-"
},
{
"path": "actions/dependencytrack.go",
"chars": 1856,
"preview": "package actions\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\tdtrack \"github.com/D"
},
{
"path": "actions/dependencytrack_test.go",
"chars": 4668,
"preview": "package actions\n\nimport (\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.co"
},
{
"path": "actions/docker.go",
"chars": 3647,
"preview": "package actions\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/tidwall/gjson\"\n\n\t\""
},
{
"path": "actions/docker_test.go",
"chars": 9963,
"preview": "package actions\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/docker/docker/api/type"
},
{
"path": "actions/email.go",
"chars": 4158,
"preview": "package actions\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/smtp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/a"
},
{
"path": "actions/email_test.go",
"chars": 4329,
"preview": "package actions\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/smtp\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc mockSend(e"
},
{
"path": "actions/example/exec/defectdojo-curl-upload-scan.sh",
"chars": 2525,
"preview": "#!/usr/bin/env sh\n\n# this shell script is meant to be executed by a Aquasec/Postee \"exec\"\n# action, the event data is pa"
},
{
"path": "actions/exec.go",
"chars": 1563,
"preview": "package actions\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\n\t\""
},
{
"path": "actions/exec_test.go",
"chars": 1822,
"preview": "package actions\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"os/exec\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"githu"
},
{
"path": "actions/goldens/validbody.txt",
"chars": 16,
"preview": "foo bar baz body"
},
{
"path": "actions/http.go",
"chars": 2726,
"preview": "package actions\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"regexp\""
},
{
"path": "actions/http_test.go",
"chars": 5219,
"preview": "package actions\n\nimport (\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/tes"
},
{
"path": "actions/jira.go",
"chars": 14989,
"preview": "package actions\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"strconv\"\n\n\t\"github.com/aquasecurity/poste"
},
{
"path": "actions/jira_test.go",
"chars": 30978,
"preview": "package actions\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n\n\t\"github"
},
{
"path": "actions/kubernetes.go",
"chars": 4920,
"preview": "package actions\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v"
},
{
"path": "actions/kubernetes_test.go",
"chars": 6886,
"preview": "package actions\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/test"
},
{
"path": "actions/message.go",
"chars": 1127,
"preview": "package actions\n\nimport (\n\t\"bytes\"\n\t\"log\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nconst po"
},
{
"path": "actions/message_test.go",
"chars": 1839,
"preview": "package actions\n\nimport (\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n\n\t\"github.com/aquasecurity/postee/v2/f"
},
{
"path": "actions/nexusiq.go",
"chars": 4405,
"preview": "package actions\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"s"
},
{
"path": "actions/nexusiq_test.go",
"chars": 3759,
"preview": "package actions\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gorilla/mu"
},
{
"path": "actions/opsgenie.go",
"chars": 3172,
"preview": "package actions\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/opsgenie/opsgenie-go-sdk-v2"
},
{
"path": "actions/opsgenie_test.go",
"chars": 2563,
"preview": "package actions\n\nimport (\n\t\"testing\"\n\n\t\"github.com/opsgenie/opsgenie-go-sdk-v2/alert\"\n\t\"github.com/stretchr/testify/asse"
},
{
"path": "actions/pagerduty.go",
"chars": 1747,
"preview": "package actions\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/PagerDuty/go-pagerduty\"\n\t\"github.com/aquasecuri"
},
{
"path": "actions/pagerduty_test.go",
"chars": 3105,
"preview": "package actions\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testif"
},
{
"path": "actions/plugin.go",
"chars": 1067,
"preview": "package actions\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\nconst (\n\tApplicationS"
},
{
"path": "actions/servicenow.go",
"chars": 2196,
"preview": "package actions\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\""
},
{
"path": "actions/slack.go",
"chars": 2920,
"preview": "package actions\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"log\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/dat"
},
{
"path": "actions/splunk.go",
"chars": 3651,
"preview": "package actions\n\nimport (\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"net/http"
},
{
"path": "actions/stdout.go",
"chars": 609,
"preview": "package actions\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecurity/postee"
},
{
"path": "actions/teams.go",
"chars": 2244,
"preview": "package actions\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aquasecu"
},
{
"path": "actions/testdata/nexus-iq-sbom.xml",
"chars": 1667,
"preview": "<?xml version=\"1.0\"?>\n<bom serialNumber=\"urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79\" version=\"1\"\n xmlns=\"http://cy"
},
{
"path": "actions/webhook.go",
"chars": 2009,
"preview": "package actions\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2"
},
{
"path": "actions/webhook_test.go",
"chars": 3020,
"preview": "package actions\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/asse"
},
{
"path": "cfg.yaml",
"chars": 8263,
"preview": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant "
},
{
"path": "config/cfg-actions.yaml",
"chars": 3104,
"preview": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant "
},
{
"path": "config/cfg-controller-runner.yaml",
"chars": 2824,
"preview": "name: Postee Controller Runner Demo\n\naqua-server: # URL of Aqua Server for links. E.g. https://myserver.aqua"
},
{
"path": "config/cfg-docker-actions.yaml",
"chars": 1686,
"preview": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant "
},
{
"path": "config/cfg-k8s-actions.yaml",
"chars": 1762,
"preview": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant "
},
{
"path": "config/cfg-trivy-aws.yaml",
"chars": 419,
"preview": "actions:\n - type: awssecurityhub\n enable: true\n name: Send Findings to Security Hub\nroutes:\n - name: Send Trivy "
},
{
"path": "config/cfg-trivy-operator-defectdojo.yaml",
"chars": 1322,
"preview": "# The configuration file contains a general settings section,\n# routes, templates and actions sections.\n\nname: tenant "
},
{
"path": "config/cfg-trivy-operator.yaml",
"chars": 469,
"preview": "routes:\n- name: Trivy Operator Alerts\n input: input.report.summary.criticalCount > 0 # You can customize this based on "
},
{
"path": "config/terminate-malicious-pods.yaml",
"chars": 962,
"preview": "actions:\n - type: webhook\n name: Send Message to Webhook\n enable: true\n url: http://foo.com\n - type: exec\n "
},
{
"path": "controller/controller.go",
"chars": 3825,
"preview": "package controller\n\nimport (\n\tgotls \"crypto/tls\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"gith"
},
{
"path": "data/inpteval.go",
"chars": 229,
"preview": "package data\n\ntype Inpteval interface {\n\tEval(in map[string]interface{}, serverUrl string) (map[string]string, error)\n\tB"
},
{
"path": "data/slack.go",
"chars": 285,
"preview": "package data\n\ntype SlackTextBlock struct {\n\tTypeField string `json:\"type\"`\n\tTextField string `json:\"text\"`\n}\n\ntype Slack"
},
{
"path": "data/types.go",
"chars": 2146,
"preview": "package data\n\ntype ScanImageInfo struct {\n\tImage string `json:\"image\"`\n\tRegistry string `"
},
{
"path": "data/utils.go",
"chars": 170,
"preview": "package data\n\nimport (\n\t\"regexp\"\n)\n\nfunc ClearField(source string) string {\n\tre := regexp.MustCompile(`[[:cntrl:]]|[\\x{F"
},
{
"path": "data/utils_test.go",
"chars": 358,
"preview": "package data\n\nimport (\n\t\"testing\"\n)\n\nfunc TestClearField(t *testing.T) {\n\ttests := []struct {\n\t\tin, out string\n\t}{\n\t\t{\"t"
},
{
"path": "dbservice/actions.go",
"chars": 938,
"preview": "package dbservice\n\nimport (\n\t\"time\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc MayBeStoreMessage(message []byte, messageKey strin"
},
{
"path": "dbservice/changedbpath_test.go",
"chars": 314,
"preview": "package dbservice\n\nimport \"testing\"\n\nfunc TestChangeDbPath(t *testing.T) {\n\ttestPath := \"/tmp/test.db\"\n\tstoredPath := Db"
},
{
"path": "dbservice/checker.go",
"chars": 2037,
"preview": "package dbservice\n\nimport (\n\t\"bytes\"\n\t\"github.com/aquasecurity/postee/v2/utils\"\n\t\"log\"\n\t\"time\"\n\n\tbolt \"go.etcd.io/bbolt\""
},
{
"path": "dbservice/checker_test.go",
"chars": 5500,
"preview": "package dbservice\n\nimport (\n\t\"github.com/stretchr/testify/assert\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\n"
},
{
"path": "dbservice/dbaggregator.go",
"chars": 1312,
"preview": "package dbservice\n\nimport (\n\t\"encoding/json\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc AggregateScans(output string,\n\tcurrentSca"
},
{
"path": "dbservice/dbaggregator_test.go",
"chars": 2134,
"preview": "package dbservice\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestAggregateScans(t *testing.T) {\n\tvar (\n\t\tscan1 = map[string]stri"
},
{
"path": "dbservice/dbparam.go",
"chars": 1033,
"preview": "package dbservice\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar (\n\tdbBucketName = \"WebhookBucke"
},
{
"path": "dbservice/dbparam_test.go",
"chars": 1468,
"preview": "package dbservice\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestSetNewDbPathFromEnv(t *testing.T) {"
},
{
"path": "dbservice/dbservice_test.go",
"chars": 4453,
"preview": "package dbservice\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"go.etcd.io/bbolt\"\n)\n\nvar (\n\tAlpineImageKey = \"sha25"
},
{
"path": "dbservice/delete.go",
"chars": 340,
"preview": "package dbservice\n\nimport bolt \"go.etcd.io/bbolt\"\n\nfunc dbDelete(db *bolt.DB, bucket string, keys [][]byte) error {\n\tret"
},
{
"path": "dbservice/init.go",
"chars": 218,
"preview": "package dbservice\n\nimport \"go.etcd.io/bbolt\"\n\nvar Init = func(db *bbolt.DB, bucket string) error {\n\treturn db.Update(fun"
},
{
"path": "dbservice/insert.go",
"chars": 306,
"preview": "package dbservice\n\nimport bolt \"go.etcd.io/bbolt\"\n\nvar dbInsert = func(db *bolt.DB, bucket string, key, value []byte) er"
},
{
"path": "dbservice/invalidinit_test.go",
"chars": 1659,
"preview": "package dbservice\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"testing\"\n\n\t\"go.etcd.io/bbolt\"\n)\n\nvar tests = []struct {\n\tcaseDesc s"
},
{
"path": "dbservice/plgnstats.go",
"chars": 642,
"preview": "package dbservice\n\nimport (\n\t\"strconv\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc RegisterPlgnInvctn(name string) error {\n\tmutex."
},
{
"path": "dbservice/plgnstats_test.go",
"chars": 1251,
"preview": "package dbservice\n\nimport (\n\t\"os\"\n\t\"strconv\"\n\t\"testing\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nfunc TestRegisterPlgnInvctn(t *test"
},
{
"path": "dbservice/select.go",
"chars": 337,
"preview": "package dbservice\n\nimport (\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nvar dbSelect = func(db *bolt.DB, bucket, key string) (result []b"
},
{
"path": "dbservice/sharedcfg.go",
"chars": 1405,
"preview": "package dbservice\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\t\"os\"\n\n\tbolt \"go.etcd.io/bbolt\"\n)\n\nconst (\n\ta"
},
{
"path": "dbservice/sharedcfg_test.go",
"chars": 1283,
"preview": "package dbservice\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestApiKey(t *testing.T) {\n\tdbPathReal := DbPath\n\tdefer func() {\n\t\t"
},
{
"path": "deploy/helm/postee/.helmignore",
"chars": 349,
"preview": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation"
},
{
"path": "deploy/helm/postee/Chart.yaml",
"chars": 609,
"preview": "apiVersion: v2\nname: postee\ndescription: A Helm chart for Postee\ntype: application\n\n# Versions are expected to follow Se"
},
{
"path": "deploy/helm/postee/templates/NOTES.txt",
"chars": 1944,
"preview": "1. Get the application URL by running these commands:\n{{- if .Values.ingress.enabled }}\n{{- range $host := .Values.ingre"
},
{
"path": "deploy/helm/postee/templates/_helpers.tpl",
"chars": 2627,
"preview": "{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"postee.name\" -}}\n{{- default .Chart.Name .Values.nameOverride | trun"
},
{
"path": "deploy/helm/postee/templates/cfg-secret.yaml",
"chars": 224,
"preview": "{{ if not .Values.configuration.existingSecret.enabled }}\napiVersion: v1\nkind: Secret\nmetadata:\n name: {{ include \"post"
},
{
"path": "deploy/helm/postee/templates/ingress.yaml",
"chars": 1652,
"preview": "{{- if .Values.ingress.enabled -}}\n{{- $fullName := include \"postee.fullname\" . -}}\n{{- $svcPort := .Values.service.port"
},
{
"path": "deploy/helm/postee/templates/postee-svc.yaml",
"chars": 523,
"preview": "apiVersion: v1\nkind: Service\nmetadata:\n name: {{ include \"postee.fullname\" . }}\n labels:\n {{- include \"postee.label"
},
{
"path": "deploy/helm/postee/templates/postee-ui-secret.yaml",
"chars": 361,
"preview": "{{- if not .Values.posteUi.existingSecret.enabled }}\napiVersion: v1\nkind: Secret\nmetadata:\n name: {{ include \"postee.ui"
},
{
"path": "deploy/helm/postee/templates/postee-ui-svc.yaml",
"chars": 401,
"preview": "apiVersion: v1\nkind: Service\nmetadata:\n name: {{ include \"postee.ui.fullname\" . }}\n labels:\n {{- include \"postee.ui"
},
{
"path": "deploy/helm/postee/templates/postee-ui.yaml",
"chars": 3653,
"preview": "{{- $fullName := include \"postee.fullname\" . -}}\napiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: {{ include \"post"
},
{
"path": "deploy/helm/postee/templates/postee.yaml",
"chars": 4774,
"preview": "{{- $fullName := include \"postee.fullname\" . -}}\napiVersion: apps/v1\nkind: StatefulSet\nmetadata:\n name: {{ include \"pos"
},
{
"path": "deploy/helm/postee/templates/pvc.yaml",
"chars": 503,
"preview": "{{- if and .Values.persistentVolume.enabled (not .Values.persistentVolume.existingClaim) }}\n{{- $fullName := include \"po"
},
{
"path": "deploy/helm/postee/templates/serviceaccount.yaml",
"chars": 318,
"preview": "{{- if .Values.serviceAccount.create -}}\napiVersion: v1\nkind: ServiceAccount\nmetadata:\n name: {{ include \"postee.servic"
},
{
"path": "deploy/helm/postee/templates/tests/test-connection.yaml",
"chars": 376,
"preview": "apiVersion: v1\nkind: Pod\nmetadata:\n name: \"{{ include \"postee.fullname\" . }}-test-connection\"\n labels:\n {{- include"
},
{
"path": "deploy/helm/postee/values.yaml",
"chars": 12271,
"preview": "# Default values for postee.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\nrep"
},
{
"path": "deploy/kubernetes/hostPath/postee-pv.yaml",
"chars": 1126,
"preview": "#Create the volume for the Postee volumeClaimTemplates\n---\napiVersion: v1\nkind: PersistentVolume\nmetadata:\n name: poste"
},
{
"path": "deploy/kubernetes/postee-actions.yaml",
"chars": 10177,
"preview": "## postee-configmap\n---\nkind: ConfigMap\napiVersion: v1\nmetadata:\n name: postee-config\ndata:\n cfg.yaml: |\n ---\n #"
},
{
"path": "deploy/kubernetes/postee-controller.yaml",
"chars": 9607,
"preview": "## postee-configmap\n---\nkind: ConfigMap\napiVersion: v1\nmetadata:\n name: postee-controller-config\ndata:\n cfg.yaml: |\n "
},
{
"path": "deploy/kubernetes/postee-runner.yaml",
"chars": 4818,
"preview": "## postee-configmap\n---\nkind: ConfigMap\napiVersion: v1\nmetadata:\n name: postee-runner-config\ndata:\n client-cert.pem: |"
},
{
"path": "deploy/kubernetes/postee.yaml",
"chars": 8259,
"preview": "## postee-configmap\n---\nkind: ConfigMap\napiVersion: v1\nmetadata:\n name: postee-config\ndata:\n cfg.yaml: |\n ---\n #"
},
{
"path": "docker-compose.yml",
"chars": 737,
"preview": "version: \"3\"\nservices:\n posteewebhook:\n build:\n context: .\n dockerfile: Dockerfile\n "
},
{
"path": "docs/actions/actions.md",
"chars": 7742,
"preview": "# Postee Actions\n\n## Motivation\nProper alert management can help security practitioners make informed decisions about th"
},
{
"path": "docs/actions.md",
"chars": 19578,
"preview": "## Motivation\nProper alert management can help security practitioners make informed decisions about their codebase. Howe"
},
{
"path": "docs/advanced.md",
"chars": 3433,
"preview": "This page covers some advanced topics that the experienced users of Postee might like to try. \n\n## Using environment var"
},
{
"path": "docs/aquacloud.md",
"chars": 1922,
"preview": "## Configure the Aqua Server with Webhook Integration\nPostee can be integrated with Aqua Console to deliver vulnerabilit"
},
{
"path": "docs/blueprints/devops-pagerduty.md",
"chars": 1354,
"preview": "# Paging DevOps Teams\n\n## Introduction\nIn this walkthrough, we will setup vulnerability scanning with [Trivy](https://gi"
},
{
"path": "docs/blueprints/external-healthcheck.md",
"chars": 2861,
"preview": "# Distributed Service Healthcheck\n\n## Introduction\nIn this walkthrough, we will setup a globally distributed healthcheck"
},
{
"path": "docs/blueprints/image-processing.md",
"chars": 2256,
"preview": "# Doing Serverless Image Recognition using Postee Actions and AWS\n\n## Introduction\nIn this walkthrough, we will setup Po"
},
{
"path": "docs/blueprints/trivy-aws-security-hub.md",
"chars": 2712,
"preview": "# Trivy AWS CSPM Scanning\n\n## Introduction\nIn this walkthrough, we will setup AWS Cloud Scanning with [Trivy](https://gi"
},
{
"path": "docs/blueprints/trivy-operator.md",
"chars": 1948,
"preview": "# Trivy Operator \n\n## Introduction\nIn this walk through, configure [Trivy Operator](https://github.com/aquasecurity/triv"
},
{
"path": "docs/blueprints/trivy-vulnerability-scan.md",
"chars": 1902,
"preview": "# Trivy Vulnerability Scan\n\n## Introduction\nIn this walkthrough, we will setup vulnerability scanning with [Trivy](https"
},
{
"path": "docs/config.md",
"chars": 541,
"preview": "When Postee receives a message it will process it based on routing rules and send it to the appropriate target. How does"
},
{
"path": "docs/controller-runner.md",
"chars": 8818,
"preview": "# Controller Runner Mode\n\n## Introduction\nPostee can also be run in Controller/Runner mode. The idea is to decouple enfo"
},
{
"path": "docs/demo.md",
"chars": 2750,
"preview": "In this demo, we’ll walk through a scenario in which a user wants to act on a security event received from Tracee, an op"
},
{
"path": "docs/deployment.md",
"chars": 681,
"preview": "# Deployment\n\n## Kubernetes\n\nDue to a limitation in how persistent volumes are handled in EKS, we have to ensure that bo"
},
{
"path": "docs/examples.md",
"chars": 5476,
"preview": "Here are some Postee configuration samples to showcase a variety of use cases.\n\n??? example \"Forward all \"Block\" audit e"
},
{
"path": "docs/improvements.md",
"chars": 1776,
"preview": "# Improvements\n\nPostee like any other software isn't perfect and as the writing of this document can be improved in the "
},
{
"path": "docs/index.md",
"chars": 703,
"preview": "#\n\n<figure markdown>\n { align=\"center\" }\n</figure>\n\n\nPostee is a simple message routing a"
},
{
"path": "docs/install.md",
"chars": 4133,
"preview": "To run Postee you will first need to configure the [Postee Configuration File](/postee/config), which contains all the m"
},
{
"path": "docs/routes.md",
"chars": 5451,
"preview": "A route is used to control message flows. Each route includes the input message condition, the template that should be u"
},
{
"path": "docs/settings.md",
"chars": 1329,
"preview": "General settings are specified at the root level of cfg.yaml. They include general configuration that applies to the Pos"
},
{
"path": "docs/templates.md",
"chars": 3351,
"preview": "Templates are used to format input messages before sending them to the action. For example - before sending a message to"
},
{
"path": "docs/troubleshooting-of-rego-templates.md",
"chars": 0,
"preview": ""
},
{
"path": "docs/ui.md",
"chars": 1494,
"preview": "Postee provides a simple Web UI to simplify the configuration management.\n\n\n\n"
},
{
"path": "formatting/eval.go",
"chars": 2424,
"preview": "package formatting\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity"
},
{
"path": "formatting/eval_test.go",
"chars": 4841,
"preview": "package formatting\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nvar"
},
{
"path": "formatting/htmlprovider.go",
"chars": 1360,
"preview": "package formatting\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype HtmlProvider struct{}\n\nfunc (html *HtmlProvider) P(p str"
},
{
"path": "formatting/htmlprovider_test.go",
"chars": 876,
"preview": "package formatting\n\nimport (\n\t\"testing\"\n)\n\nfunc TestHtmlProvider_Table(t *testing.T) {\n\tvar tests = []tableTest{\n\t\t{\n\t\t\t"
},
{
"path": "formatting/jiraprovider.go",
"chars": 1095,
"preview": "package formatting\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype JiraLayoutProvider struct{}\n\nfunc (jira *JiraLayoutProvi"
},
{
"path": "formatting/jiraprovider_test.go",
"chars": 677,
"preview": "package formatting\n\nimport \"testing\"\n\nfunc TestJiraLayoutProvider_Tags(t *testing.T) {\n\ttests := []tagsTest{\n\t\t{\n\t\t\t\"Lor"
},
{
"path": "formatting/markup_test.go",
"chars": 1667,
"preview": "package formatting\n\nimport (\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/layout\"\n)\n\ntype tagsTest struct {\n\tsource "
},
{
"path": "formatting/slackmrkdwnprovider.go",
"chars": 3267,
"preview": "package formatting\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc "
},
{
"path": "formatting/slackmrkdwnprovider_test.go",
"chars": 1864,
"preview": "package formatting\n\nimport \"testing\"\n\nfunc TestSlackMrkdwn(t *testing.T) {\n\ttests := []tagsTest{\n\t\t{\n\t\t\t\"Lorem Ipsum\",\n\t"
},
{
"path": "go.mod",
"chars": 5197,
"preview": "module github.com/aquasecurity/postee/v2\n\ngo 1.18\n\nrequire (\n\tgithub.com/DependencyTrack/client-go v0.11.0\n\tgithub.com/P"
},
{
"path": "go.sum",
"chars": 81494,
"preview": "cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1"
},
{
"path": "integration/controller_runner_test.go",
"chars": 3123,
"preview": "//go:build integration\n\npackage integration\n\nimport (\n\t\"io/ioutil\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2"
},
{
"path": "integration/goldens/client-cert.pem",
"chars": 1558,
"preview": "-----BEGIN CERTIFICATE-----\nMIIEUjCCArqgAwIBAgIRAPLnJ75aAxz0TfngEX3vEikwDQYJKoZIhvcNAQELBQAw\nZzEeMBwGA1UEChMVbWtjZXJ0IGR"
},
{
"path": "integration/goldens/client-key.pem",
"chars": 1704,
"preview": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5UuSFQ++xFlqJ\nNTgc8P5Oyzb5BLMpElX1PWNCr26"
},
{
"path": "integration/goldens/rootCA.pem",
"chars": 1663,
"preview": "-----BEGIN CERTIFICATE-----\nMIIEnjCCAwagAwIBAgIRAOPa8EEi4WZW/wxKT9ad82AwDQYJKoZIhvcNAQELBQAw\nZzEeMBwGA1UEChMVbWtjZXJ0IGR"
},
{
"path": "integration/goldens/server-cert.pem",
"chars": 1489,
"preview": "-----BEGIN CERTIFICATE-----\nMIIEHjCCAoagAwIBAgIQKa6srh/okxC7U4sW60pAvzANBgkqhkiG9w0BAQsFADBn\nMR4wHAYDVQQKExVta2NlcnQgZGV"
},
{
"path": "integration/goldens/server-key.pem",
"chars": 1704,
"preview": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCbAtKoPEH738mK\nlgPNSgTzxoyjUHs7d9SCDuk5BLu"
},
{
"path": "integration/goldens/simple.yaml",
"chars": 601,
"preview": "name: Test\n\nroutes:\n- name: terminate-and-notify\n input: contains(input.SigMetadata.ID, \"TRC-2\")\n actions: [terminate-"
},
{
"path": "integration/goldens/test-seed.txt",
"chars": 116,
"preview": "SUAGAA3TNI36JHTD6GLFJRR6KZIY7YXS2ZISHQA4LPZZZG2D6KG5JPV7DM\nUBUQ63VFZEW3IS7RGQQZF5DIT2FTCMTZAAHFENK3G5M6ADRZ5WAJLAQN\n"
},
{
"path": "layout/assurances.go",
"chars": 601,
"preview": "package layout\n\nimport (\n\t\"strconv\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc RenderAssurances(provider LayoutP"
},
{
"path": "layout/colors.go",
"chars": 269,
"preview": "package layout\n\nfunc CriticalColor() string { return \"#c00000\" }\nfunc HighColor() string { return \"#e0443d\" }\nfu"
},
{
"path": "layout/malware.go",
"chars": 456,
"preview": "package layout\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc RenderMalware(malware []"
},
{
"path": "layout/provider.go",
"chars": 259,
"preview": "package layout\n\ntype LayoutProvider interface {\n\tTitleH1(title string) string\n\tTitleH2(title string) string\n\tTitleH3(tit"
},
{
"path": "layout/sensitive.go",
"chars": 430,
"preview": "package layout\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc RenderSensitiveData(sensitive []dat"
},
{
"path": "layout/ticketLayout.go",
"chars": 2711,
"preview": "package layout\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nfunc GenTestDescription(provi"
},
{
"path": "layout/vulnerabilities.go",
"chars": 2092,
"preview": "package layout\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n)\n\nconst empty = \"none\"\n\nfunc Re"
},
{
"path": "main.go",
"chars": 5142,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"runtime\"\n\t\"syscall\"\n\n\t\"github.com/aquasecurity/po"
},
{
"path": "mkdocs.yml",
"chars": 2441,
"preview": "site_name: Postee\nsite_url: https://aquasecurity.github.io/postee\nsite_description: Integrate vulnerability scanning wit"
},
{
"path": "msgservice/aggregatebytime_test.go",
"chars": 1831,
"preview": "package msgservice\n\nimport (\n\t\"log\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\"github.com/aquasecu"
},
{
"path": "msgservice/aggregatescan_test.go",
"chars": 2418,
"preview": "package msgservice\n\nimport (\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquas"
},
{
"path": "msgservice/applicationscopeowner_test.go",
"chars": 1652,
"preview": "package msgservice\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"githu"
},
{
"path": "msgservice/calculateexpired_test.go",
"chars": 397,
"preview": "package msgservice\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCalculateExpired(t *t"
},
{
"path": "msgservice/getuniqueid_test.go",
"chars": 6089,
"preview": "package msgservice\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/poste"
},
{
"path": "msgservice/logs.go",
"chars": 256,
"preview": "package msgservice\n\nimport \"log\"\n\nfunc prnInputLogs(msg string, v ...interface{}) {\n\tmaxLen := 20\n\tfor idx, e := range v"
},
{
"path": "msgservice/msghandling.go",
"chars": 5066,
"preview": "package msgservice\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\""
},
{
"path": "msgservice/msgservice_mocks_test.go",
"chars": 2879,
"preview": "package msgservice\n\nimport (\n\t\"log\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/aquasecurity/postee/v2/formatting\"\n\t\"github.com/aqu"
},
{
"path": "msgservice/msgservice_scan_test.go",
"chars": 6408,
"preview": "package msgservice\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/data\"\n\t\"github.com/aq"
},
{
"path": "msgservice/msgservice_test.go",
"chars": 4797,
"preview": "package msgservice\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github"
},
{
"path": "msgservice/regocriteria_test.go",
"chars": 3136,
"preview": "package msgservice\n\nimport (\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/aquasecurity/postee/v2/dbservice\"\n\t\"github.com/aquas"
},
{
"path": "msgservice/scheduler.go",
"chars": 1419,
"preview": "package msgservice\n\nimport (\n\t\"log\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\"github.com/aquasecurity/post"
},
{
"path": "msgservice/scheduler_test.go",
"chars": 1343,
"preview": "package msgservice\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/aquasecurity/postee/v2/actions\"\n\t\"github.com/aquas"
},
{
"path": "msgservice/testdata/all-in-one-image.json",
"chars": 84797,
"preview": "{\n \"image\": \"all-in-one:3.5.19223\",\n \"registry\": \"Aqua\",\n \"scan_started\": {\n \"seconds\": 1624544066,\n "
},
{
"path": "msgservice/testdata/collection-of-interfaces.json",
"chars": 82,
"preview": "{\n \"arr\": [\n {\n \"foo\": \"bar\"\n },\n {\n \"foo\": \"bar2\"\n }\n ]\n}"
},
{
"path": "msgservice/uniquemsgkey.go",
"chars": 897,
"preview": "package msgservice\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst (\n\tpropSep = \".\"\n)\n\nfunc GetMessageUniqueId(in map[string]interf"
},
{
"path": "overrides/main.html",
"chars": 189,
"preview": "{% extends \"base.html\" %}\n\n{% block outdated %}\nYou're not viewing the latest version.\n<a href=\"{{ '../' ~ base_url }}\">"
},
{
"path": "rego-filters/Allow-Image-Name.rego",
"chars": 278,
"preview": "package postee\n\n\nArrayPermitedImageNames := {\"ubuntu\", \"busybox\"}#Comma separated list of images that will trigger the i"
},
{
"path": "rego-filters/Allow-Registry.rego",
"chars": 251,
"preview": "package postee\n\n\nArrayPermitedRegistry := {\"Aqua\"} #The list of registry name that triggers the integration.\n\ndefault Pe"
},
{
"path": "rego-filters/Credential Access",
"chars": 337,
"preview": "package postee\n\n\nArrayBlockedSignaturesCredentialAccessCredentialAccess := {\n \"TRC-8\", \"TRC-10\"\n}\n\ndefault BlockedSig"
},
{
"path": "rego-filters/Defense Evasion",
"chars": 321,
"preview": "package postee\n\n\nArrayBlockedSignaturesDefenseEvation := {\n \"TRC-2\", \"TRC-3\", \"TRC-4\", \"TRC-9\", \"TRC-5\"\n}\n\ndefault Bl"
},
{
"path": "rego-filters/Ignore-Image-Name.rego",
"chars": 283,
"preview": "package postee\n\n\nArrayIgnoredImageNames := {\"alpine\", \"postgres\"} #List of comma separated images that will be ignored b"
},
{
"path": "rego-filters/Ignore-Registry.rego",
"chars": 264,
"preview": "package postee\n\n\nArrayIgnoreRegistry := {\"Aqua\"} #Comma separated list of registries that will be ignored by the integra"
},
{
"path": "rego-filters/Initial Access",
"chars": 281,
"preview": "package postee\n\n\nArrayBlockedSignaturesInitialAccess := {\n \"TRC-12\"\n}\n\ndefault BlockedSignaturesInitialAccess = false"
},
{
"path": "rego-filters/Persistence",
"chars": 280,
"preview": "package postee\n\n\nArrayBlockedSignaturesPersistence := {\n \"TRC-7\", \"TRC-15\"\n}\n\ndefault BlockedSignaturesPersistence = "
},
{
"path": "rego-filters/Policy-Min-Vulnerability.rego",
"chars": 497,
"preview": "package postee\nimport future.keywords.in\n\n#Constants vulnerability values. Don't remove it!\nallVulnerability := {\"neglig"
},
{
"path": "rego-filters/Policy-Only-Fix-Available.rego",
"chars": 373,
"preview": "package postee\n\n#Trigger the integration only if image has a vulnerability with fix available (true). \n#If set to false,"
},
{
"path": "rego-filters/Policy-Related-Features.rego",
"chars": 1740,
"preview": "package postee\nimport future.keywords.in\n#Constants vulnerability values. Don't remove it!\nallVulnerability := {\"negligi"
},
{
"path": "rego-filters/Privilege Escalation",
"chars": 321,
"preview": "package postee\n\n\nArrayBlockedSignaturesPrivilegeEscalation := {\n \"TRC-11\", \"TRC-14\"\n}\n\ndefault BlockedSignaturesPrivi"
},
{
"path": "rego-filters/Tracee Default Set",
"chars": 354,
"preview": "package postee\n\n\nArrayBlockedSignatures := {\n \"TRC-1\", \"TRC-2\", \"TRC-3\", \"TRC-4\", \"TRC-5\", \"TRC-6\", \"TRC-7\", \"TRC-8\""
},
{
"path": "rego-filters/Trivy AWS Findings",
"chars": 96,
"preview": "package postee\n\nallow {\n contains(input.Findings[0].ProductFields[\"Product Name\"], \"Trivy\")\n}"
},
{
"path": "rego-templates/common/common.rego",
"chars": 523,
"preview": "package postee\n############################################# Common functions ##########################################"
},
{
"path": "rego-templates/example/audit-html.rego",
"chars": 152,
"preview": "package example.audit.html\n\n#Example of handling audit user\n\ntitle:=\"Audit event received\"\nresult:=sprintf(\"Audit event "
},
{
"path": "rego-templates/example/defectdojo/trivy-operator-defectdojo.rego",
"chars": 1833,
"preview": "# METADATA\n# title: trivy-operator-defectdojo\n# scope: package\npackage plejd.trivyoperator.defectdojo\n\ntitle:=\"-\" #not u"
},
{
"path": "rego-templates/example/defectdojo/trivy-operator-defectdojo_test.rego",
"chars": 853,
"preview": "package plejd.trivyoperator.defectdojo.test\n\nimport data.plejd.trivyoperator.defectdojo.result\n\n\ntest_a_allowed {\n\n inp"
},
{
"path": "rego-templates/raw-message-html.rego",
"chars": 199,
"preview": "package postee.rawmessage.html\n\n\ntitle:=\"Raw Message Received\"\n\n# Postee injects custom function jsonformat() to pretty "
},
{
"path": "rego-templates/raw-message-json.rego",
"chars": 93,
"preview": "package postee.rawmessage.json\n\n\ntitle:=\"-\" #not used with webhook\n\nresult:=jsonformat(input)"
},
{
"path": "rego-templates/servicenow-incident.rego",
"chars": 3418,
"preview": "package postee.servicenow.incident\n\nimport future.keywords\nimport data.postee.by_flag\nimport data.postee.with_default\n\n#"
},
{
"path": "rego-templates/servicenow-insight.rego",
"chars": 7876,
"preview": "package postee.servicenow.insight\n\nimport future.keywords\nimport future.keywords.if\nimport data.postee.by_flag\nimport da"
},
{
"path": "rego-templates/servicenow.rego",
"chars": 9524,
"preview": "package postee.servicenow\n\nimport future.keywords\nimport future.keywords.if\nimport data.postee.by_flag\nimport data.poste"
},
{
"path": "rego-templates/tracee-html.rego",
"chars": 416,
"preview": "package postee.tracee.html\n\n#Example of handling tracee event\n\ntitle:=sprintf(\"Tracee Detection - %s\", [input.SigMetadat"
}
]
// ... and 135 more files (download for full content)
About this extraction
This page contains the full source code of the aquasecurity/postee GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 335 files (1.0 MB), approximately 349.2k tokens, and a symbol index with 631 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.