Full Code of aquasecurity/postee for AI

main 4d31463ed7be cached
335 files
1.0 MB
349.2k tokens
631 symbols
1 requests
Download .txt
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]
![](https://github.com/aquasecurity/postee/workflows/Go/badge.svg)
[![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:


[![Postee Demo Video](./docs/img/postee-video-thumbnail.jpg)](https://www.youtube.com/watch?v=HZ5Z8jAVH8w)

Primary use of Postee is to act as a message relay and notification service that integrates with a variety of third-party services. Postee can also be used for sending vulnerability scan results or audit alerts from Aqua Platform to collaboration systems.

In addition, Postee can also be used to enforce pre-defined behaviours that can orchestrate actions based on input messages as triggers.

![Postee v2 scheme](docs/img/postee-v2-scheme.png)

## Status
Although we are trying to keep new releases backward compatible with previous versions, this project is still incubating,
and some APIs and code structures may change.

## Documentation
The official [Documentation] provides detailed installation, configuration, troubleshooting, and quick start guides.

---
Postee is an [Aqua Security](https://aquasec.com) open source project.  
Learn about our [Open Source Work and Portfolio].  
Join the community, and talk to us about any matter in [GitHub Discussions] or [Slack].

[Documentation]: https://aquasecurity.github.io/postee/latest
[Open Source Work and Portfolio]: https://www.aquasec.com/products/open-source-projects/
[Slack]: https://slack.aquasec.com/
[GitHub Discussions]: https://github.com/aquasecurity/postee/discussions


## Release

1. Bump version of [helm chart](https://github.com/aquasecurity/postee/blob/main/deploy/helm/postee/Chart.yaml).
1. (By repository admin) Create a new tag. Postee and helm charts are automatically released by github actions.
1. (By repository admin) Run [publish-docs workflow](https://github.com/aquasecurity/postee/blob/main/.github/workflows/publish-docs.yml), if document has been updated.


================================================
FILE: actions/aws_securityhub.go
================================================
package actions

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"strings"

	"github.com/aquasecurity/postee/v2/formatting"
	"github.com/aquasecurity/postee/v2/layout"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/securityhub"
	"github.com/aws/aws-sdk-go-v2/service/securityhub/types"
)

type securityHubAPI interface {
	BatchImportFindings(ctx context.Context, params *securityhub.BatchImportFindingsInput, optFns ...func(*securityhub.Options)) (*securityhub.BatchImportFindingsOutput, error)
}

type Finding struct {
	SchemaVersion string   `json:"SchemaVersion,omitempty"`
	ID            string   `json:"Id,omitempty"`
	ProductArn    string   `json:"ProductArn,omitempty"`
	GeneratorID   string   `json:"GeneratorId,omitempty"`
	AwsAccountID  string   `json:"AwsAccountId,omitempty"`
	Types         []string `json:"Types,omitempty"`
	CreatedAt     string   `json:"CreatedAt,omitempty"`
	UpdatedAt     string   `json:"UpdatedAt,omitempty"`
	Severity      struct {
		Label string `json:"Label,omitempty"`
	} `json:"Severity,omitempty"`
	Title       string `json:"Title,omitempty"`
	Description string `json:"Description,omitempty"`
	Remediation struct {
		Recommendation struct {
			Text string `json:"Text,omitempty"`
			URL  string `json:"Url,omitempty"`
		} `json:"Recommendation,omitempty"`
	} `json:"Remediation,omitempty"`
	ProductFields struct {
		ProductName string `json:"Product Name,omitempty"`
	} `json:"ProductFields,omitempty"`
	Resources []struct {
		Type      string `json:"Type,omitempty"`
		ID        string `json:"Id,omitempty"`
		Partition string `json:"Partition,omitempty"`
		Region    string `json:"Region,omitempty"`
		Details   struct {
			Container struct {
				ImageName string `json:"ImageName,omitempty"`
			} `json:"Container,omitempty"`
			Other struct {
				CVEID            string `json:"CVE ID,omitempty"`
				CVETitle         string `json:"CVE Title,omitempty"`
				PkgName          string `json:"PkgName,omitempty"`
				InstalledPackage string `json:"Installed Package,omitempty"`
				PatchedPackage   string `json:"Patched Package,omitempty"`
				NvdCvssScoreV3   string `json:"NvdCvssScoreV3,omitempty"`
				NvdCvssVectorV3  string `json:"NvdCvssVectorV3,omitempty"`
				NvdCvssScoreV2   string `json:"NvdCvssScoreV2,omitempty"`
				NvdCvssVectorV2  string `json:"NvdCvssVectorV2,omitempty"`
			} `json:"Other,omitempty"`
		} `json:"Details,omitempty"`
	} `json:"Resources,omitempty"`
	RecordState string `json:"RecordState,omitempty"`
}

type Report struct {
	Findings []Finding
}

type AWSSecurityHubClient struct {
	client securityHubAPI

	Name string
}

func (sh AWSSecurityHubClient) GetName() string {
	return sh.Name
}

func (sh *AWSSecurityHubClient) Init() error {
	// Load the Shared AWS Configuration (~/.aws/config)
	cfg, err := config.LoadDefaultConfig(context.TODO())
	if err != nil {
		return fmt.Errorf("failed to load AWS config: %w", err)
	}

	sh.client = securityhub.NewFromConfig(cfg)
	if sh.client == nil {
		return fmt.Errorf("failed to create AWS Security Hub client")
	}

	return nil
}

func (sh AWSSecurityHubClient) Send(m map[string]string) error {
	var r Report
	if err := json.Unmarshal([]byte(m["description"]), &r); err != nil {
		return fmt.Errorf("AWS Security Hub unmarshalling failed: %w", err)
	}

	if len(r.Findings) <= 0 {
		return fmt.Errorf("trivy AWS sent no findings to Postee, skipping sending")
	}

	var awsfindings []types.AwsSecurityFinding
	for _, f := range r.Findings {
		af := types.AwsSecurityFinding{
			AwsAccountId:  aws.String(f.AwsAccountID),
			CreatedAt:     aws.String(f.CreatedAt),
			Description:   aws.String(f.Description),
			GeneratorId:   aws.String(f.GeneratorID),
			Id:            aws.String(f.ID),
			ProductArn:    aws.String(f.ProductArn),
			SchemaVersion: aws.String(f.SchemaVersion),
			Title:         aws.String(f.Title),
			UpdatedAt:     aws.String(f.UpdatedAt),
			Types:         f.Types,
		}

		af.Resources = append(af.Resources, []types.Resource{
			{
				Id:   aws.String(f.ID),
				Type: aws.String(strings.Join(f.Types, " ")),
			},
		}...)

		af.Remediation = &types.Remediation{
			Recommendation: &types.Recommendation{
				Text: aws.String(f.Remediation.Recommendation.Text),
				Url:  aws.String(f.Remediation.Recommendation.URL),
			},
		}

		af.Severity = &types.Severity{
			Label: types.SeverityLabel(f.Severity.Label),
		}

		awsfindings = append(awsfindings, af)
	}

	var successCount, failedCount int
	awsFindingChunks := chunkBy(awsfindings, 100)
	log.Printf("sending %d findings in %d chunk(s) to AWS Security Hub", len(awsfindings), len(awsFindingChunks))
	for _, awsfindingChunk := range awsFindingChunks {
		output, err := sh.client.BatchImportFindings(context.TODO(), &securityhub.BatchImportFindingsInput{
			Findings: awsfindingChunk,
		})
		if err != nil {
			return fmt.Errorf("upload to AWS Security Hub failed: %w", err)
		}

		if len(output.FailedFindings) > 0 {
			failedCount += len(output.FailedFindings)
			log.Printf("%d findings failed to be reported...", len(output.FailedFindings))
			for _, ff := range output.FailedFindings {
				log.Printf("Failed finding details: ID: %s , ErrorCode: %s, ErrorMessage: %s\n", *ff.Id, *ff.ErrorCode, *ff.ErrorMessage)
			}
		}
		successCount += int(output.SuccessCount)
	}

	log.Printf("successfully sent: %d findings to AWS Security Hub", successCount)
	return nil
}

func (sh AWSSecurityHubClient) Terminate() error {
	return nil
}

func (sh AWSSecurityHubClient) GetLayoutProvider() layout.LayoutProvider {
	// Todo: This is MOCK. Because Formatting isn't need for Webhook
	// todo: The App should work with `return nil`
	return new(formatting.HtmlProvider)
}

func chunkBy[T any](items []T, chunkSize int) (chunks [][]T) {
	for chunkSize < len(items) {
		items, chunks = items[chunkSize:], append(chunks, items[0:chunkSize:chunkSize])
	}
	return append(chunks, items)
}


================================================
FILE: actions/aws_securityhub_test.go
================================================
package actions

import (
	"context"
	"fmt"
	"testing"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/securityhub"
	"github.com/aws/aws-sdk-go-v2/service/securityhub/types"
	"github.com/aws/smithy-go/middleware"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

const GoodFindings = `{
 "Findings": [
   {
     "SchemaVersion": "2018-10-08",
     "Id": "alpine:3.10 (alpine 3.10.9)/CVE-2021-36159",
     "ProductArn": "arn:aws:securityhub:eu-west-2::product/aquasecurity/aquasecurity",
     "GeneratorId": "Trivy/CVE-2021-36159",
     "AwsAccountId": "000000",
     "Types": [
       "Software and Configuration Checks/Vulnerabilities/CVE"
     ],
     "CreatedAt": "2022-08-05T22:29:18.549914-07:00",
     "UpdatedAt": "2022-08-10T22:29:18.549938-07:00",
     "Severity": {
       "Label": "CRITICAL"
     },
     "Title": "Trivy found a vulnerability to CVE-2021-36159 in container alpine:3.10 (alpine 3.10.9)",
     "Description": "libfetch before 2021-07-26, as used in apk-tools, xbps, and other products, mishandles numeric strings for the FTP and HTTP protocols. The FTP passive mode implementation allows an out-of-bounds read because strtol is used to parse the relevant numbers into address bytes. It does not check if the line ends prematurely. If it does, the for-loop condition checks for the &#39;\\0&#39; 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 &#39;\\0&#39; 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, "&", "&amp;")
	s = strings.ReplaceAll(s, "<", "&lt;")
	s = strings.ReplaceAll(s, ">", "&gt;")
	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-
Download .txt
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
Download .txt
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  ![Postee Logo](img/postee.png){ 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![Config app](img/postee-output-config.png)\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.

Copied to clipboard!