Skip to main content

bd-jira-sync

Report Black Duck findings as Jira tickets.

One tool for both risk types:

  • --license — FOSS license risks, grouped into one ticket per project version.
  • --security — vulnerabilities (CVEs), one ticket per finding or grouped per project version.

Both modes share the same behaviour: read the BOM, drop everything that was already reported in an earlier run, create Jira tickets for what is new, link them to a master ticket, and keep unresolved tickets of earlier runs linked to the current master ticket. Nothing is written to Jira unless --send is given.

No server names, project names or credentials live in the code: all of it comes from a configuration file and the environment.

Install

uv sync

Configure

copy config.example.yaml config.yaml
copy .env.example .env
  • .env holds the credentials — BLACKDUCK_URL, BLACKDUCK_API_TOKEN, JIRA_URL, JIRA_TOKEN. Real environment variables win over .env, so CI can inject them from a secret store.
  • config.yaml holds everything else: Jira project, issue type, components, assignee, link type, done statuses, the project versions to scan, and the reporting rules per risk type. Unknown or incomplete settings abort the run with a message naming the offending key.

Per-project risk types

A project version is scanned for both risk types unless its optional track list says otherwise. Use it when, say, all ten project versions need security tracking but only seven need license tracking:

projects:
  - project: payment-service      # no 'track' -> license + security
    version: main
  - project: vendor-sdk           # security only
    version: "4.1"
    track: [security]

Valid values are license and security. --license / --security still decide which workflows run at all; track decides which project versions each one visits. An explicit --project overrides the config file entirely, including track.

Ticket grouping

Each workflow decides on its own through group_by_project:

license:
  group_by_project: true    # default: one ticket per project version, risks in a table
security:
  group_by_project: false   # default: one ticket per vulnerability

So grouped security tickets alongside individual license tickets is just security.group_by_project: true plus license.group_by_project: false, and the opposite combination works the same way.

group_by_project License Security
true one ticket per project version, all new risks in a table, priority from the highest risk one ticket per project version, all new findings in a table
false one ticket per risky component, priority from that component's risk one ticket per vulnerability

Which summary template applies follows the same switch: summary_template / single_summary_template for license, group_summary_template / summary_template for security.

--single is a per-run override that forces false on both workflows, for the times you want individual tickets without editing the config file. Grouping only affects tickets created from now on; findings already recorded in the state file stay attached to their original ticket.

Run

# Dry run: show what would be reported (no Jira writes, no state written)
uv run bd-jira-sync ABC-123 --license --security

# Create the tickets and remember them
uv run bd-jira-sync ABC-123 --license --security --send

# Only one project version, license risks only
uv run bd-jira-sync ABC-123 --license --project my-product/main --send

# One ticket per individual risk instead of one grouped ticket per project version
uv run bd-jira-sync ABC-123 --license --security --single --send
Option Meaning
MASTER_TICKET Jira key of the ticket tracking this scan cycle; every new ticket is linked to it.
--license / --security Which risk types to report. At least one is required.
--single One ticket per finding instead of one grouped ticket per project version.
--send Actually create and link issues and write the state file. Without it the run is read-only.
-c, --config PATH Configuration file (default config.yaml).
-s, --state PATH State file (default state.yaml).
-e, --env PATH Env file with the tokens (default .env).
-p, --project NAME/VERSION Scan only this project version for every selected workflow; repeatable, overrides the config file including track.
-q, --quiet Print the summary only.

Exit codes: 0 success, 1 finished with errors, 2 bad command line, 3 configuration error, 4 Black Duck authentication failed.

State file

The state file is the memory of the tool: it maps every reported finding to the Jira ticket that covers it. Point every run of the same project set at the same file, otherwise findings are reported twice.

version: 1
license:
  my-product/main:
    project_name: my-product
    version_name: main
    tickets:
      - jira_key: ABC-456
        summary: FOSS license risks in my-product-main on CW23
        url: https://jira.example.com/browse/ABC-456
        priority: Critical
        created_at: "2026-06-04T10:12:00"
        master_tickets: [ABC-123]
        findings:
          - key: my-product|main|libfoo|1.2.3|GPL-3.0-only|HIGH
            summary: libfoo 1.2.3 [GPL-3.0-only, High]
            severity: HIGH

A finding is identified by its key, so a component whose license or risk level changes is reported again — that is a genuinely new thing to review — while an unchanged finding is never reported twice.

Layout

src/blackduck_jira_sync/
  cli.py             command line, run header, exit codes
  config.py          configuration model, validation, env overrides
  http_client.py     JSON over HTTP, TLS options, error mapping
  blackduck.py       Black Duck REST client (auth, name lookup, paging)
  jira.py            Jira REST client (create, link, status)
  state.py           the state file
  engine.py          dedup, ticket creation, linking, counters
  markup.py          Jira wiki markup helpers
  reporting.py       console output
  workflows/
    base.py          the Workflow interface
    license.py       license risk collection and ticket text
    security.py      vulnerability collection and ticket text

Adding a risk type means adding one Workflow implementation: collect findings, build ticket drafts. Deduplication, linking, state and reporting are shared.

TLS

TLS verification is on by default. verify_ssl: false exists for lab setups and disables certificate checking — do not use it against production systems.

With verify_ssl: true, Python validates certificates against the certifi bundle, not against the Windows or Linux system store. A corporate server behind a private CA — or a server that does not send its intermediate certificate — therefore fails with:

certificate verify failed: unable to get local issuer certificate

The fix is a PEM file holding the issuing CA chain, referenced from ca_bundle for blackduck and jira (each host may need a different chain; one file can hold both):

blackduck:
  verify_ssl: true
  ca_bundle: "certs/company-ca.pem"

jira:
  verify_ssl: true
  ca_bundle: "certs/company-ca.pem"

Generating the bundle

The browser trusts the server already, so the certificates can be taken from the machine's own trust store. Run this once per host and append to the same file — it writes every certificate above the server certificate (intermediates and root) as PEM.

$h = 'blackduck.example.com'   # repeat with 'jira.example.com'
$t = New-Object System.Net.Sockets.TcpClient($h, 443)
$s = New-Object System.Net.Security.SslStream($t.GetStream(), $false, ({ $true } -as [System.Net.Security.RemoteCertificateValidationCallback]))
$s.AuthenticateAsClient($h)
$leaf = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($s.RemoteCertificate)
$s.Dispose(); $t.Close()

$chain = New-Object System.Security.Cryptography.X509Certificates.X509Chain
$chain.ChainPolicy.RevocationMode = 'NoCheck'
$null = $chain.Build($leaf)

New-Item -ItemType Directory -Force -Path certs | Out-Null
$out = @()
$chain.ChainElements | Select-Object -Skip 1 | ForEach-Object {
    $c = $_.Certificate
    $out += "# $($c.Subject)"
    $out += '-----BEGIN CERTIFICATE-----'
    $out += [Convert]::ToBase64String($c.RawData, 'InsertLineBreaks')
    $out += '-----END CERTIFICATE-----'
}
Add-Content -Path certs\company-ca.pem -Value $out -Encoding ascii

On Linux or macOS, openssl does the same:

openssl s_client -showcerts -servername blackduck.example.com \
  -connect blackduck.example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -outform PEM >> certs/company-ca.pem

Verify the result before running the sync:

python -c "import requests; print(requests.get('https://jira.example.com/rest/api/2/serverInfo', verify='certs/company-ca.pem').status_code)"

Anything other than an SSLError means the bundle is accepted. If your IT department publishes the CA certificate, prefer that file over the exported one.

Development

uv run pytest

A Makefile wraps the same commands and works identically on Windows, Linux and macOS as long as GNU make and uv are installed:

make            # list the targets
make install    # uv sync --all-groups
make build      # uv build -> dist/
make test       # uv run pytest
make clean      # drop dist/, build/ and the caches

make run ARGS="MASTER-1 --license --security --send"
make test ARGS="-k license -v"

Quote ARGS, otherwise make treats the leading dashes as its own options.

License

Copyright (C) 2026 Dinesh Ravi

Licensed under the GNU Affero General Public License v3.0 or later. See LICENSE for the full text.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

blackduck_jira_sync-0.1.0.tar.gz (77.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

blackduck_jira_sync-0.1.0-py3-none-any.whl (42.1 kB view details)

Uploaded Python 3

File details

Details for the file blackduck_jira_sync-0.1.0.tar.gz.

File metadata

  • Download URL: blackduck_jira_sync-0.1.0.tar.gz
  • Upload date:
  • Size: 77.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.31 {"installer":{"name":"uv","version":"0.11.31","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for blackduck_jira_sync-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2e12656d1d0756024f789fc42dd4b1c4fd2d0be8e0cf3ab5d0c88c799fe5667c
MD5 9dfaaeeb38ad84ec7b0644c0cf755e7b
BLAKE2b-256 7c5e8adc8efdcc6e8b772b0641477bbd36c9f89a524b9c3c135f57fa9b8f204c

See more details on using hashes here.

File details

Details for the file blackduck_jira_sync-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: blackduck_jira_sync-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 42.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.31 {"installer":{"name":"uv","version":"0.11.31","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for blackduck_jira_sync-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 19acc14198a5c0bfd6c6e1b52b8ea79b9ca74fcdbf9ed446de847ff820447d3f
MD5 36f100554a1f7e49d563187a65bf4b49
BLAKE2b-256 e64cb8631e0b71b9b8e5e14fa172e4bf7860e0fa90cb3de50b6c908c33918fc4

See more details on using hashes here.

Release history Release notifications | RSS feed

0.2.5

2 files

0.2.4

2 files

0.2.3

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page