Metadata-Version: 2.5
Name: blackduck-jira-sync
Version: 0.1.0
Summary: Sync Black Duck license and security findings into Jira tickets.
License: AGPL-3.0-or-later
License-File: LICENSE
Requires-Python: >=3.9
Requires-Dist: blackduck>=1.1.3
Requires-Dist: python-dotenv>=1.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: requests>=2.31
Description-Content-Type: text/markdown

# bd-jira-sync

Report [Black Duck](https://www.blackduck.com/) 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

```powershell
uv sync
```

## Configure

```powershell
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:

```yaml
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`:

```yaml
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

```powershell
# 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.

```yaml
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):

```yaml
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.

```powershell
$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:

```bash
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:

```powershell
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

```powershell
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:

```powershell
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](LICENSE) for the full text.
