From d12e6edf240492303166b8c1b0dab14d4abb8b29 Mon Sep 17 00:00:00 2001 From: nr-compliancebot Date: Tue, 24 Jun 2025 09:02:27 -0700 Subject: [PATCH 1/6] Create org-level-trivy-scan.yml --- .github/workflows/org-level-trivy-scan.yml | 80 ++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .github/workflows/org-level-trivy-scan.yml diff --git a/.github/workflows/org-level-trivy-scan.yml b/.github/workflows/org-level-trivy-scan.yml new file mode 100644 index 0000000..b270df0 --- /dev/null +++ b/.github/workflows/org-level-trivy-scan.yml @@ -0,0 +1,80 @@ +name: 'Organization Vulnerability Scan' + +on: + workflow_call: + inputs: + image-name: + description: 'The full name of the Docker image to scan' + required: true + type: string + secrets: + TEAM_SLACK_WEBHOOK_URL: + description: 'Slack webhook for team-specific vulnerability alerts' + required: false + +jobs: + vulnerability-scan: + name: Trivy Vulnerability Scan + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Trivy for console output (All Vulnerabilities) + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ inputs.image-name }} + scan-type: 'image' + severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN' + ignore-unfixed: false + format: 'table' + exit-code: '0' + + - name: Run Trivy for SARIF report (Fixed Vulnerabilities Only) + id: trivy_sarif_scan + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ inputs.image-name }} + scan-type: 'image' + severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-results.sarif' + exit-code: '1' + continue-on-error: true + + - name: Upload Trivy SARIF report to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + category: 'trivy' + if: always() + + - name: Send Slack Notification on Failure + if: steps.trivy_sarif_scan.outcome == 'failure' && secrets.TEAM_SLACK_WEBHOOK_URL + uses: slackapi/slack-github-action@v1.26.0 + with: + payload: | + { + "blocks": [ + { + "type": "section", + "text": { "type": "mrkdwn", "text": "🚨 *Vulnerability Alert in `${{ github.repository }}`*" } + }, + { + "type": "section", + "text": { "type": "mrkdwn", "text": "High or critical vulnerabilities with an available fix were detected." } + }, + { + "type": "actions", + "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Workflow Run" }, "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" } ] + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.TEAM_SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK From 07d6304a3a2b841c3af4031255c5be1005847d9b Mon Sep 17 00:00:00 2001 From: daniellim1 <173089175+daniellim1@users.noreply.github.com> Date: Thu, 10 Jul 2025 07:08:37 -0700 Subject: [PATCH 2/6] Update org-level-trivy-scan.yml --- .github/workflows/org-level-trivy-scan.yml | 31 +++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/org-level-trivy-scan.yml b/.github/workflows/org-level-trivy-scan.yml index b270df0..e72d4b7 100644 --- a/.github/workflows/org-level-trivy-scan.yml +++ b/.github/workflows/org-level-trivy-scan.yml @@ -18,12 +18,23 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + packages: read security-events: write + env: + SLACK_WEBHOOK_URL_SET: ${{ secrets.TEAM_SLACK_WEBHOOK_URL != '' }} + steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Run Trivy for console output (All Vulnerabilities) uses: aquasecurity/trivy-action@master with: @@ -47,15 +58,29 @@ jobs: exit-code: '1' continue-on-error: true + # Check if the SARIF file has content + - name: Check if SARIF report has content + id: check_sarif + run: | + # If the trivy-results.sarif file exists and is larger than a few bytes (i.e., not empty), + # set the output 'sarif_has_content' to true. + if [ -s trivy-results.sarif ] && [ $(wc -c < trivy-results.sarif) -gt 2 ]; then + echo "sarif_has_content=true" >> $GITHUB_OUTPUT + else + echo "sarif_has_content=false" >> $GITHUB_OUTPUT + fi + if: always() # Ensure this check runs even if the previous step failed + + # Upload on condition - name: Upload Trivy SARIF report to GitHub Security + # checks the output if ignore-unfixed is true throws an output + if: steps.check_sarif.outputs.sarif_has_content == 'true' uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' - category: 'trivy' - if: always() - name: Send Slack Notification on Failure - if: steps.trivy_sarif_scan.outcome == 'failure' && secrets.TEAM_SLACK_WEBHOOK_URL + if: steps.trivy_sarif_scan.outcome == 'failure' && env.SLACK_WEBHOOK_URL_SET == 'true' uses: slackapi/slack-github-action@v1.26.0 with: payload: | From 52c6db3920513511b09c813402d3c2b28b2c73d8 Mon Sep 17 00:00:00 2001 From: daniellim1 <173089175+daniellim1@users.noreply.github.com> Date: Thu, 10 Jul 2025 07:24:55 -0700 Subject: [PATCH 3/6] Update org-level-trivy-scan.yml --- .github/workflows/org-level-trivy-scan.yml | 126 +++++++-------------- 1 file changed, 44 insertions(+), 82 deletions(-) diff --git a/.github/workflows/org-level-trivy-scan.yml b/.github/workflows/org-level-trivy-scan.yml index e72d4b7..041f9aa 100644 --- a/.github/workflows/org-level-trivy-scan.yml +++ b/.github/workflows/org-level-trivy-scan.yml @@ -1,105 +1,67 @@ -name: 'Organization Vulnerability Scan' +name: 'Build and Scan' on: - workflow_call: - inputs: - image-name: - description: 'The full name of the Docker image to scan' - required: true - type: string - secrets: - TEAM_SLACK_WEBHOOK_URL: - description: 'Slack webhook for team-specific vulnerability alerts' - required: false + pull_request: + schedule: + # Runs daily at midnight UTC + - cron: '0 0 * * *' jobs: - vulnerability-scan: - name: Trivy Vulnerability Scan + # JOB 1: Build the Docker image using the custom script and push it to GHCR + build: + name: 'Build and Push Docker Image' runs-on: ubuntu-latest permissions: contents: read - packages: read - security-events: write + packages: write # Required to push images to GHCR + outputs: + image-name: ${{ steps.tag-and-push.outputs.image-name }} + env: - SLACK_WEBHOOK_URL_SET: ${{ secrets.TEAM_SLACK_WEBHOOK_URL != '' }} + DOCKER_IMAGE: newrelic/infrastructure-bundle + DOCKER_IMAGE_TAG: ci steps: - - name: Checkout repository + - name: 'Checkout code' uses: actions/checkout@v4 - - name: Log in to GitHub Container Registry + - name: 'Log in to GitHub Container Registry' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - - name: Run Trivy for console output (All Vulnerabilities) - uses: aquasecurity/trivy-action@master + + - name: 'Set up Go' + uses: actions/setup-go@v5 with: - image-ref: ${{ inputs.image-name }} - scan-type: 'image' - severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN' - ignore-unfixed: false - format: 'table' - exit-code: '0' + go-version-file: go.mod - - name: Run Trivy for SARIF report (Fixed Vulnerabilities Only) - id: trivy_sarif_scan - uses: aquasecurity/trivy-action@master - with: - image-ref: ${{ inputs.image-name }} - scan-type: 'image' - severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN' - ignore-unfixed: true - format: 'sarif' - output: 'trivy-results.sarif' - exit-code: '1' - continue-on-error: true + - name: 'Download integrations' + run: go run downloader.go - # Check if the SARIF file has content - - name: Check if SARIF report has content - id: check_sarif - run: | - # If the trivy-results.sarif file exists and is larger than a few bytes (i.e., not empty), - # set the output 'sarif_has_content' to true. - if [ -s trivy-results.sarif ] && [ $(wc -c < trivy-results.sarif) -gt 2 ]; then - echo "sarif_has_content=true" >> $GITHUB_OUTPUT - else - echo "sarif_has_content=false" >> $GITHUB_OUTPUT - fi - if: always() # Ensure this check runs even if the previous step failed + - name: 'Build local Docker image with custom script' + run: DOCKER_PLATFORMS=linux/amd64 ./docker-build.sh . --load - # Upload on condition - - name: Upload Trivy SARIF report to GitHub Security - # checks the output if ignore-unfixed is true throws an output - if: steps.check_sarif.outputs.sarif_has_content == 'true' - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: 'trivy-results.sarif' + - name: 'Tag and Push image to GHCR' + id: tag-and-push + run: | + IMAGE_ID=ghcr.io/${{ github.repository }}:${{ github.sha }} + docker tag ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_IMAGE_TAG }} $IMAGE_ID + docker push $IMAGE_ID + echo "image-name=$IMAGE_ID" >> $GITHUB_OUTPUT - - name: Send Slack Notification on Failure - if: steps.trivy_sarif_scan.outcome == 'failure' && env.SLACK_WEBHOOK_URL_SET == 'true' - uses: slackapi/slack-github-action@v1.26.0 - with: - payload: | - { - "blocks": [ - { - "type": "section", - "text": { "type": "mrkdwn", "text": "🚨 *Vulnerability Alert in `${{ github.repository }}`*" } - }, - { - "type": "section", - "text": { "type": "mrkdwn", "text": "High or critical vulnerabilities with an available fix were detected." } - }, - { - "type": "actions", - "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Workflow Run" }, "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" } ] - } - ] - } - env: - SLACK_WEBHOOK_URL: ${{ secrets.TEAM_SLACK_WEBHOOK_URL }} - SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK + # JOB 2: Call the reusable organization workflow + scan: + name: 'Trigger Organization Scan' + needs: build + permissions: + security-events: write # Required to allow the called workflow to upload SARIF results + + # --- This line is updated to point to your new test org --- + uses: newrelic-trivy/.github/.github/workflows/required-trivy-scan.yml@main + + with: + image-name: ${{ needs.build.outputs.image-name }} + secrets: inherit From 014f8c4322070eaa5e41cf27336d7850dfafd2ac Mon Sep 17 00:00:00 2001 From: daniellim1 <173089175+daniellim1@users.noreply.github.com> Date: Thu, 10 Jul 2025 07:36:54 -0700 Subject: [PATCH 4/6] Update org-level-trivy-scan.yml --- .github/workflows/org-level-trivy-scan.yml | 126 ++++++++++++++------- 1 file changed, 82 insertions(+), 44 deletions(-) diff --git a/.github/workflows/org-level-trivy-scan.yml b/.github/workflows/org-level-trivy-scan.yml index 041f9aa..e72d4b7 100644 --- a/.github/workflows/org-level-trivy-scan.yml +++ b/.github/workflows/org-level-trivy-scan.yml @@ -1,67 +1,105 @@ -name: 'Build and Scan' +name: 'Organization Vulnerability Scan' on: - pull_request: - schedule: - # Runs daily at midnight UTC - - cron: '0 0 * * *' + workflow_call: + inputs: + image-name: + description: 'The full name of the Docker image to scan' + required: true + type: string + secrets: + TEAM_SLACK_WEBHOOK_URL: + description: 'Slack webhook for team-specific vulnerability alerts' + required: false jobs: - # JOB 1: Build the Docker image using the custom script and push it to GHCR - build: - name: 'Build and Push Docker Image' + vulnerability-scan: + name: Trivy Vulnerability Scan runs-on: ubuntu-latest permissions: contents: read - packages: write # Required to push images to GHCR + packages: read + security-events: write - outputs: - image-name: ${{ steps.tag-and-push.outputs.image-name }} - env: - DOCKER_IMAGE: newrelic/infrastructure-bundle - DOCKER_IMAGE_TAG: ci + SLACK_WEBHOOK_URL_SET: ${{ secrets.TEAM_SLACK_WEBHOOK_URL != '' }} steps: - - name: 'Checkout code' + - name: Checkout repository uses: actions/checkout@v4 - - name: 'Log in to GitHub Container Registry' + - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - - name: 'Set up Go' - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - - name: 'Download integrations' - run: go run downloader.go + - name: Run Trivy for console output (All Vulnerabilities) + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ inputs.image-name }} + scan-type: 'image' + severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN' + ignore-unfixed: false + format: 'table' + exit-code: '0' - - name: 'Build local Docker image with custom script' - run: DOCKER_PLATFORMS=linux/amd64 ./docker-build.sh . --load + - name: Run Trivy for SARIF report (Fixed Vulnerabilities Only) + id: trivy_sarif_scan + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ inputs.image-name }} + scan-type: 'image' + severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN' + ignore-unfixed: true + format: 'sarif' + output: 'trivy-results.sarif' + exit-code: '1' + continue-on-error: true - - name: 'Tag and Push image to GHCR' - id: tag-and-push + # Check if the SARIF file has content + - name: Check if SARIF report has content + id: check_sarif run: | - IMAGE_ID=ghcr.io/${{ github.repository }}:${{ github.sha }} - docker tag ${{ env.DOCKER_IMAGE }}:${{ env.DOCKER_IMAGE_TAG }} $IMAGE_ID - docker push $IMAGE_ID - echo "image-name=$IMAGE_ID" >> $GITHUB_OUTPUT + # If the trivy-results.sarif file exists and is larger than a few bytes (i.e., not empty), + # set the output 'sarif_has_content' to true. + if [ -s trivy-results.sarif ] && [ $(wc -c < trivy-results.sarif) -gt 2 ]; then + echo "sarif_has_content=true" >> $GITHUB_OUTPUT + else + echo "sarif_has_content=false" >> $GITHUB_OUTPUT + fi + if: always() # Ensure this check runs even if the previous step failed - # JOB 2: Call the reusable organization workflow - scan: - name: 'Trigger Organization Scan' - needs: build - permissions: - security-events: write # Required to allow the called workflow to upload SARIF results - - # --- This line is updated to point to your new test org --- - uses: newrelic-trivy/.github/.github/workflows/required-trivy-scan.yml@main - - with: - image-name: ${{ needs.build.outputs.image-name }} - secrets: inherit + # Upload on condition + - name: Upload Trivy SARIF report to GitHub Security + # checks the output if ignore-unfixed is true throws an output + if: steps.check_sarif.outputs.sarif_has_content == 'true' + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + - name: Send Slack Notification on Failure + if: steps.trivy_sarif_scan.outcome == 'failure' && env.SLACK_WEBHOOK_URL_SET == 'true' + uses: slackapi/slack-github-action@v1.26.0 + with: + payload: | + { + "blocks": [ + { + "type": "section", + "text": { "type": "mrkdwn", "text": "🚨 *Vulnerability Alert in `${{ github.repository }}`*" } + }, + { + "type": "section", + "text": { "type": "mrkdwn", "text": "High or critical vulnerabilities with an available fix were detected." } + }, + { + "type": "actions", + "elements": [ { "type": "button", "text": { "type": "plain_text", "text": "View Workflow Run" }, "url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" } ] + } + ] + } + env: + SLACK_WEBHOOK_URL: ${{ secrets.TEAM_SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOK From c59e8a0e75d6f002762dda7f1853b4a7a102cfb3 Mon Sep 17 00:00:00 2001 From: daniellim1 <173089175+daniellim1@users.noreply.github.com> Date: Wed, 16 Jul 2025 09:38:35 -0700 Subject: [PATCH 5/6] Update org-level-trivy-scan.yml --- .github/workflows/org-level-trivy-scan.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/org-level-trivy-scan.yml b/.github/workflows/org-level-trivy-scan.yml index e72d4b7..5a52cae 100644 --- a/.github/workflows/org-level-trivy-scan.yml +++ b/.github/workflows/org-level-trivy-scan.yml @@ -45,6 +45,16 @@ jobs: format: 'table' exit-code: '0' + - name: Run Trivy for console output (All Vulnerabilities) unfixed + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ inputs.image-name }} + scan-type: 'image' + severity: 'CRITICAL,HIGH,MEDIUM,LOW,UNKNOWN' + ignore-unfixed: true + format: 'table' + exit-code: '0' + - name: Run Trivy for SARIF report (Fixed Vulnerabilities Only) id: trivy_sarif_scan uses: aquasecurity/trivy-action@master From 817912af3e221dbc9a787746c95750a8dd250a0e Mon Sep 17 00:00:00 2001 From: hjha Date: Mon, 25 May 2026 17:25:41 +0530 Subject: [PATCH 6/6] added org-wide user ban setup --- .github/workflows/ban-user-org.yml | 140 +++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 .github/workflows/ban-user-org.yml diff --git a/.github/workflows/ban-user-org.yml b/.github/workflows/ban-user-org.yml new file mode 100644 index 0000000..7a5c3aa --- /dev/null +++ b/.github/workflows/ban-user-org.yml @@ -0,0 +1,140 @@ +name: Ban user on /ban comment (org required workflow) + +# Org-wide version of ban-user.yml, intended to run as a GitHub +# "required workflow" so every repo in the org gets /ban without each +# repo having to copy the file. +# +# Setup (org owner): +# 1. Put this file in a repo in the org (e.g. /.github-private or any +# repo allowed to host required workflows). +# 2. Settings → Actions → General → Required workflows → add this file and +# target the repos it should apply to (typically: all repos). +# 3. Register the GitHub App once at the org level and store the secrets as +# organization secrets (Settings → Secrets and variables → Actions → +# Organization secrets), scoped to the repos the required workflow runs in: +# BAN_APP_ID — app id of a GitHub App installed on the org +# BAN_APP_PRIVATE_KEY — PEM private key for that App +# +# App permissions: +# Organization: Administration (read & write) — to block users +# Repository: Metadata (read), Pull requests (read), Issues (write) +# +# The App must be installed on the org and granted access to every repo the +# required workflow targets. The /ban command itself is gated on the +# commenter having admin permission on the specific repo where the comment +# was posted, so org-wide deployment does not widen who can ban. + +on: + issue_comment: + types: [created] + +permissions: + issues: write + pull-requests: write + +jobs: + ban: + if: > + github.event.issue.pull_request != null && + startsWith(github.event.comment.body, '/ban') + runs-on: ubuntu-latest + steps: + - name: Validate /ban command + id: validate + env: + BODY: ${{ github.event.comment.body }} + run: | + set -euo pipefail + first_line=$(printf '%s' "${BODY}" | head -n1) + if printf '%s' "${first_line}" | grep -Eq '^/ban(\s|$)'; then + echo "match=true" >> "$GITHUB_OUTPUT" + else + echo "Comment starts with /ban prefix but is not the /ban command; skipping." + echo "match=false" >> "$GITHUB_OUTPUT" + fi + + - name: Check commenter has admin permission on the repo + if: steps.validate.outputs.match == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMENTER: ${{ github.event.comment.user.login }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + permission=$(gh api "repos/${REPO}/collaborators/${COMMENTER}/permission" --jq '.permission') + echo "Commenter ${COMMENTER} has permission: ${permission}" + if [[ "${permission}" != "admin" ]]; then + echo "::error::/ban requires admin permission on the repo; ${COMMENTER} has '${permission}'." + exit 1 + fi + + - name: Mint GitHub App token + if: steps.validate.outputs.match == 'true' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.BAN_APP_ID }} + private-key: ${{ secrets.BAN_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + + - name: Block PR author from the org + if: steps.validate.outputs.match == 'true' + id: block + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + ORG: ${{ github.repository_owner }} + TARGET: ${{ github.event.issue.user.login }} + ACTOR: ${{ github.event.comment.user.login }} + run: | + set -euo pipefail + + if [[ "${TARGET}" == "${ACTOR}" ]]; then + echo "::error::Refusing to self-ban (${ACTOR})." + exit 1 + fi + + target_role=$(gh api "orgs/${ORG}/memberships/${TARGET}" --jq '.role' 2>/dev/null || echo "not_a_member") + if [[ "${target_role}" == "admin" ]]; then + echo "::error::Refusing to ban ${TARGET}: they are an org admin." + exit 1 + fi + + echo "Blocking ${TARGET} from ${ORG} (requested by ${ACTOR})..." + gh api --method PUT "orgs/${ORG}/blocks/${TARGET}" + echo "Blocked ${TARGET}." + + - name: React to the /ban comment + if: always() && steps.validate.outputs.match == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + COMMENT_ID: ${{ github.event.comment.id }} + run: | + set -euo pipefail + reaction='-1' + if [[ "${{ steps.block.outcome }}" == "success" ]]; then + reaction='+1' + fi + gh api --method POST \ + "repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" \ + -f "content=${reaction}" >/dev/null + + - name: Post outcome comment on the PR + if: always() && steps.validate.outputs.match == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR: ${{ github.event.issue.number }} + TARGET: ${{ github.event.issue.user.login }} + ACTOR: ${{ github.event.comment.user.login }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + if [[ "${{ steps.block.outcome }}" == "success" ]]; then + body=":no_entry: @${TARGET} has been blocked from the ${{ github.repository_owner }} org by @${ACTOR}." + else + body=":warning: \`/ban\` invoked by @${ACTOR} did not complete. See [workflow run](${RUN_URL})." + fi + gh api --method POST \ + "repos/${REPO}/issues/${PR}/comments" \ + -f "body=${body}" >/dev/null