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
.envholds 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.yamlholds 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e12656d1d0756024f789fc42dd4b1c4fd2d0be8e0cf3ab5d0c88c799fe5667c
|
|
| MD5 |
9dfaaeeb38ad84ec7b0644c0cf755e7b
|
|
| BLAKE2b-256 |
7c5e8adc8efdcc6e8b772b0641477bbd36c9f89a524b9c3c135f57fa9b8f204c
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19acc14198a5c0bfd6c6e1b52b8ea79b9ca74fcdbf9ed446de847ff820447d3f
|
|
| MD5 |
36f100554a1f7e49d563187a65bf4b49
|
|
| BLAKE2b-256 |
e64cb8631e0b71b9b8e5e14fa172e4bf7860e0fa90cb3de50b6c908c33918fc4
|