↓ Skip to main content
Security

From finding to fix: Black Duck pull requests on GitHub Enterprise Server

A vulnerability report that nobody reads is a smoke alarm with no battery. The goal is not more findings. The goal is a pull request that a developer can review and merge before lunch.

Why this post
#

SentinelOne’s overview of GitHub vulnerability management describes the job as “discovering, triaging, and remediating security vulnerabilities in GitHub repositories, dependencies, and workflows”. It lists the usual native tools: Dependabot, code scanning, secret scanning and security advisories. It also admits the gaps: no asset inventory or risk scoring, and dependencies that Dependabot cannot track.

Most of that article assumes github.com. My question was narrower. On a GitHub Enterprise Server (GHES) instance in your own data centre, running a commercial scanner such as Black Duck, how do you get from “scan found something” to “here is a pull request that fixes it”?

This post covers the plumbing that makes that work: what GHES provides, what Black Duck provides, and a small script for the cases where neither opens a PR for you.

First, name the tools correctly
#

“Black Duck” is a company and a product family, and the names get mixed up in conversations about SAST.

ProductTypeFindsCan it open a fix PR?
Black Duck SCASoftware composition analysisVulnerable open-source dependenciesYes, for vulnerable direct dependencies (docs)
CoverityStatic analysis (SAST)Flaws in your own code (injection, null dereference, resource leaks)No; it posts PR comments (docs)
PolarisHosted platform running bothBothThrough the same action

The difference matters. A dependency fix is mechanical: change 2.14.1 to 2.17.2 in pom.xml. A SAST fix means rewriting logic, and no scanner should push that straight to a branch without a person in the loop. So the realistic design is:

  • SCA findings: let the scanner raise the fix PR.
  • SAST findings: surface them as code scanning alerts and PR comments. Automate a PR only for the narrow classes where the fix is deterministic and you have written the transform yourself.

What changes on a self-hosted GHES
#

GHES is not github.com running in your basement. Four differences shape the design.

1. Runners are yours. GHES has no GitHub-hosted runners, so every job runs on a self-hosted runner. The runner needs network access to your GHES API, to your Black Duck server and to wherever the scanner’s CLI is downloaded from.

2. Marketplace actions are not automatically available. GHES ships with the official actions/* actions. To use blackduck-inc/black-duck-security-scan, either enable GitHub Connect (automatic access to github.com actions, with policies to restrict them) or copy the action in with the actions-sync tool if the instance is air-gapped (GitHub docs).

3. Code scanning is licensed and switched on by a site administrator. Uploading third-party results needs code scanning enabled on the appliance (docs), and code scanning is part of the paid GitHub Code Security product. Dependabot alerts are included in all plans (docs).

4. Dependabot needs a link to the outside world. Dependabot alerts on GHES need GitHub Connect and the dependency graph. Advisory data syncs from the GitHub Advisory Database once an hour. Dependabot security updates (its own PRs) also need TLS and dedicated self-hosted runners, and are not available on clustered installs (docs).

The fourth point is why many enterprises bring in Black Duck in the first place. When the appliance cannot connect to github.com, or policy requires a licence and vulnerability database already approved internally, Dependabot is not available and something else has to raise the PRs.

The flow
#

Each step below is a numbered stage in the workflow.

  1. A push to main triggers a full Black Duck SCA scan on a self-hosted runner.
  2. The scan writes a SARIF report and uploads it to GHES code scanning. Findings show up in the Security tab next to CodeQL and any other tool.
  3. For each vulnerable direct dependency that has upgrade guidance, the action opens a fix PR.
  4. On every pull request, a rapid scan comments on newly introduced issues only.
  5. Coverity runs on pull requests too and comments on new SAST issues.

SARIF (Static Analysis Results Interchange Format) is the JSON format code scanning reads. Any tool that can produce it can feed the Security tab, which is how one dashboard can show results from more than one scanner.

The workflow
#

The inputs below come from Black Duck’s action reference for action v2. Pin the exact release you synced to your instance.

# .github/workflows/blackduck.yml
name: black-duck
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  sca:
    runs-on: [self-hosted, linux]
    permissions:
      contents: write          # fix PRs push a branch
      pull-requests: write     # fix PRs and PR comments
      security-events: write   # SARIF upload to code scanning
    steps:
      - uses: actions/checkout@v5

      # Pull request: rapid scan, comment on new issues only
      - name: Black Duck SCA (PR)
        if: ${{ github.event_name == 'pull_request' }}
        uses: blackduck-inc/black-duck-security-scan@v2
        with:
          blackducksca_url: ${{ vars.BLACKDUCK_URL }}
          blackducksca_token: ${{ secrets.BLACKDUCK_TOKEN }}
          blackducksca_scan_full: false
          blackducksca_prComment_enabled: true
          github_token: ${{ secrets.GITHUB_TOKEN }}

      # Main: full scan, SARIF to code scanning, fix PRs for critical/high
      - name: Black Duck SCA (full)
        if: ${{ github.event_name != 'pull_request' }}
        uses: blackduck-inc/black-duck-security-scan@v2
        env:
          DETECT_PROJECT_NAME: ${{ github.event.repository.name }}
        with:
          blackducksca_url: ${{ vars.BLACKDUCK_URL }}
          blackducksca_token: ${{ secrets.BLACKDUCK_TOKEN }}
          blackducksca_scan_full: true
          blackducksca_reports_sarif_create: true
          blackducksca_reports_sarif_severities: 'Critical,High'
          blackducksca_upload_sarif_report: true
          blackducksca_fixpr_enabled: true
          blackducksca_fixpr_maxCount: 5
          blackducksca_fixpr_filter_severities: 'CRITICAL,HIGH'
          blackducksca_fixpr_useUpgradeGuidance: 'SHORT_TERM,LONG_TERM'
          github_token: ${{ secrets.GITHUB_TOKEN }}

A few details to watch, all from the vendor documentation:

  • Fix PRs and SARIF come only from full scans. SARIF creation is not supported on PR scans, so the split between the two steps is required, not a matter of style.
  • useUpgradeGuidance picks the target version. SHORT_TERM is the nearest version without the vulnerability; LONG_TERM is the version with the fewest known issues. The first value wins. If Black Duck has no guidance, no PR is created.
  • maxCount caps PRs per branch. Start small. Twenty PRs on the first day is how a team learns to ignore a bot.
  • PR comments need a baseline. A full scan of the target branch must already exist on the Black Duck server.
  • GHES version. Black Duck says SARIF upload “has been verified on Enterprise Server version 3.15”. Newer versions should work because the upload API is the same, but test on yours.
  • Input spelling. The vendor pages show both blackducksca_prcomment_enabled and blackducksca_prComment_enabled. Check against the release you pin.

The token trap
#

The workflow above uses GITHUB_TOKEN, and that is fine for a first test. It causes two problems in practice.

First, a repository setting, “Allow GitHub Actions to create and approve pull requests”, must be on, and the default token is often read-only (docs).

Second, and less obvious: “events triggered by the GITHUB_TOKEN will not create a new workflow run” (docs). A fix PR opened with that token does not run your build or tests. The PR sits there with no checks, and a reviewer has to guess whether the upgrade broke anything. That makes a fix PR less useful than it looks.

Use a GitHub App installed on the organisation, with contents, pull_requests and security_events permissions. Mint an installation token at the start of the job and pass it as github_token. PRs then trigger CI like any other PR, and the audit log shows a named bot instead of a generic token.

When the scanner cannot open the PR: a small script
#

Coverity, an in-house linter or a rule you wrote yourself will not open fix PRs. If the fix is deterministic, for example a lint autofix, a banned-function replacement or a config hardening change, you can raise the PR yourself with two REST calls against the GHES API.

On a GHES runner, GITHUB_API_URL is already set to https://HOSTNAME/api/v3 (docs), so the same script works on any instance.

#!/usr/bin/env bash
# raise-fix-pr.sh: commit the working-tree fix on a branch and open (or reuse) a PR.
# Usage: raise-fix-pr.sh <rule-id> <title> <body-file>
set -euo pipefail

rule="$1"; title="$2"; body_file="$3"
base="${GITHUB_BASE_REF:-main}"
branch="security/fix-${rule}"
owner="${GITHUB_REPOSITORY%%/*}"
auth=(-H "Authorization: Bearer ${GH_TOKEN}" -H "Accept: application/vnd.github+json")

if git diff --quiet; then
  echo "No changes for ${rule}; nothing to raise."; exit 0
fi

git config user.name  "security-bot"
git config user.email "security-bot@example.com"
git switch -C "$branch"
git commit -am "fix(security): ${title}"
git push --force-with-lease origin "$branch"

# One open PR per rule: if it exists, the push above already updated it.
existing=$(curl -fsS "${auth[@]}" \
  "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls?state=open&head=${owner}:${branch}" \
  | jq 'length')
if [ "$existing" -gt 0 ]; then
  echo "PR for ${branch} already open; updated."; exit 0
fi

jq -n --arg t "$title" --rawfile b "$body_file" --arg h "$branch" --arg base "$base" \
  '{title:$t, body:$b, head:$h, base:$base, draft:true}' \
| curl -fsS "${auth[@]}" -X POST \
    "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/pulls" -d @- \
| jq -r '.html_url'

Three design choices are worth explaining:

  • One branch per rule, not per finding. Fifty instances of the same weak-hash call become one PR that a reviewer reads once. Re-running the job updates that PR instead of adding a duplicate.
  • Draft by default. The bot proposes and a person decides. Automated fixes should start as drafts until the team trusts that rule.
  • The fix is a separate step. The script only does the Git and GitHub part. The transform (sed, a codemod, a linter’s --fix) runs before it, so each rule’s logic stays small and testable.

The pulls endpoint and the head=owner:branch filter are documented in the GHES REST reference.

If the tool produces SARIF but has no action, upload it directly with POST /repos/{owner}/{repo}/code-scanning/sarifs, sending the file gzipped and base64-encoded:

sarif=$(gzip -c results.sarif | base64 -w0)
jq -n --arg s "$sarif" --arg sha "$GITHUB_SHA" --arg ref "$GITHUB_REF" \
  '{commit_sha:$sha, ref:$ref, sarif:$s, tool_name:"my-sast"}' \
| curl -fsS -H "Authorization: Bearer ${GH_TOKEN}" -X POST \
    "${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/code-scanning/sarifs" -d @-

The API accepts it with a 202 and processes it asynchronously. Poll /code-scanning/sarifs/{id} for processing_status. Limits apply: 20 runs per file, and only the top 5,000 results per run by severity are kept (docs). If two tools upload for the same commit, give each a distinct category (runAutomationDetails.id), or the second upload replaces the first.

Guard rails I would not skip
#

  • Branch protection on main: required reviews and required status checks. A bot PR goes through the same checks as a human PR. The SentinelOne article lists this too.
  • Severity filter first, widen later. Start with CRITICAL,HIGH and a small maxCount. Add MEDIUM once the team merges bot PRs quickly.
  • CODEOWNERS on lock files and build files so dependency PRs reach someone who understands the upgrade.
  • Time-box the queue. A fix PR open for a month is worse than none, because it gives the impression the issue is handled. Report on the age of open bot PRs as well as the alert count.

Takeaways
#

  • On GHES, check four things before designing the flow: self-hosted runners, access to actions (GitHub Connect or actions-sync), a Code Security licence for code scanning, and whether Dependabot can reach github.com.
  • Black Duck SCA can raise fix PRs for vulnerable direct dependencies; Coverity (SAST) comments on PRs. Expect automated PRs for dependencies and reviewer-led fixes for code.
  • Send every scanner’s results to code scanning as SARIF so there is one place to triage.
  • Use a GitHub App token, not GITHUB_TOKEN, so fix PRs run CI.
  • For your own rules, a 30-line script plus one branch per rule gives deduplicated, draft fix PRs on any GHES instance.

References
#

Related on this site: DAST, the runtime counterpart to the static scanning here.

Author
Samarthya
Engineer, learner, tinkerer. This is my trophy shelf of everyday work and leisure.

Related

Security standards, plainly, part 3: SLSA v1.2

A sealed medicine bottle tells you two things: who made it, and that nobody has opened it since. You do not need to test the pills yourself. SLSA gives software the same seal and the same label. Part 1 (SSDF) listed what a secure process should do. Part 2 (SAMM) measured how well you do it. Both mostly look at the code. SLSA looks at the journey from code to the thing you ship, because attackers have learned that it is often easier to tamper with a build than to find a bug.

Security standards, plainly, part 4: CISA KEV and BOD 26-04

A hospital emergency room does not treat patients in the order they arrive, or by how loudly they shout. A triage nurse asks a few quick questions and sends the bleeding patient in first. CISA’s KEV catalog and directive BOD 26-04 are triage for vulnerabilities. The first three parts of this series, SSDF, SAMM and SLSA, share one gap: none of them says how fast a vulnerability must be fixed. This part is about the clock.

Security standards, plainly, part 2: OWASP SAMM v2.0

A fitness tracker does not make you fit. It tells you where you are today, lets you pick a goal you can reach, and shows whether you are moving towards it. OWASP SAMM is a fitness tracker for how an organisation builds secure software. In part 1 NIST’s SSDF gave us a list of what a secure development process should include. It did not say how well you do each thing, or what to improve first. That is the gap SAMM fills.