Skip to content

Ticket Sync (Jira / GitHub Issues)

Availability: marketed as Pro and Enterprise · Tenant-scoped

See the Tier & Scope Matrix for why "marketed" and "enforced" differ here.

Ticket Sync automatically creates an external ticket (Jira issue or GitHub issue) when a new finding appears, keeps that ticket's content and priority in sync as the finding changes, closes it when the finding is resolved, and escalates it if the finding breaches its SLA. It runs as a per-schedule notification channel - there is currently no dedicated Settings UI page for it; it's configured through a schedule's ticket_sync config block plus service-level credentials.


How a finding becomes a ticket

Ticket Sync listens for three event types on the same event stream every other notification channel (Slack, email, webhook) consumes:

Event Emitted when Ticket Sync action
new_finding A scheduled diff run finds a finding not seen in the prior run for that org Creates a ticket (or, if one already exists for that finding+provider, updates it instead of creating a duplicate)
resolved_finding A diff run finds a fingerprint that was open in the baseline and is no longer present Closes/transitions the linked ticket
escalated_finding Either a diff-driven severity increase, or the hourly SLA-breach check (see below) Updates the ticket's priority/labels to reflect the new severity

Before doing anything, the plugin checks:

  1. ticket_sync.enabled is true in the schedule's config. This flag alone turns ticket sync on for a schedule. The deployment's TICKET_SYNC_ENABLED (chart notification.ticketSync.enabled) neither turns it on nor off (see the environment variables).
  2. The configured provider has its required target set - project_key for Jira, repo for GitHub Issues. If the target is missing, the event is skipped before any HTTP call is made.
  3. If the schedule brings no credential of its own, its target is on the deployment's allowlist (see the environment variables). Otherwise the event fails before any HTTP call.
  4. The finding's severity meets or exceeds severity_threshold (default medium).

A failure anywhere in this path never blocks delivery to other notification channels (Slack/email) for the same event. Each outcome is recorded in the run's notification delivery log (the run Overview, and Admin > Operations > Deliveries):

Outcome Recorded as
The schedule does not enable ticket_sync Nothing - no row
Unknown provider; missing project_key / repo; finding below the threshold; event carries no finding id; nothing to close or escalate; the ticket is already closed; persistence unavailable skipped, with the reason
The provider did not create the ticket, or refused an escalation failed
A target or credential the service refuses: a target the deployment's credential may not be sent to (when the schedule brings none of its own), a malformed repo, or a schedule credential that cannot be resolved failed (not retried)

The providers' update and close calls still log an HTTP failure and return, so those two are recorded as sent (platform#475).


Provider configuration

Jira

Field Source Notes
jira_domain schedule config or JIRA_DOMAIN env var e.g. mycompany → mycompany.atlassian.net
jira_email schedule config or JIRA_EMAIL env var Account used for Basic auth
jira_api_token schedule config or JIRA_API_TOKEN env var Jira Cloud API token
project_key schedule config (ticket_sync.project_key) e.g. SEC - required, no fallback
jira_close_transition_id schedule config or service-level default Workflow transition ID used to close an issue. No default exists - Jira Cloud transition IDs are per-project/per-workflow. If unset, closing a resolved finding's ticket fails loudly (logged error, no API call) rather than guessing an ID that might be wrong for your workflow.

Issues are created with summary prefixed [SEVERITY], an ADF-formatted description (finding description + finding ID + resource ID), issue type Bug, priority mapped from severity (critical→Highest, high→High, medium→Medium, low→Low, info→Lowest), and labels vsutra, security.

GitHub Issues

Field Source Notes
github_token schedule config or GITHUB_ISSUES_TOKEN env var Needs issue read/write access on the target repo
repo schedule config (ticket_sync.repo) owner/repo - required, no fallback

Issues are created with title [SEVERITY] <finding title>, a Markdown body (finding ID, severity, resource, check ID, scanner, description), and labels security, vsutra. Severity escalation is expressed as a single priority:* label (GitHub Issues has no native priority field) - any existing priority:* label is removed before the new one is added, so an issue never carries two contradictory priority labels.

Choosing/overriding the provider

ticket_sync.provider ("jira" or "github_issues") selects the provider; it falls back to the service-level TICKET_SYNC_PROVIDER env var if unset on the schedule. ticket_sync.severity_threshold falls back to TICKET_SYNC_SEVERITY_THRESHOLD (service default: medium).

Two independent GitHub issue-creation paths

The older GITHUB_ISSUES_ENABLED auto-create channel and this ticket_sync provider are separate code paths that can both create a GitHub issue for the same finding if both are active for the same repo/schedule scope. If you enable ticket_sync with provider: github_issues, make sure GITHUB_ISSUES_ENABLED is not also driving issue creation for that same scope.


Closing and reopening - the GitHub side

GitHub Issues sync is bidirectional: closing or reopening the linked GitHub issue updates the finding's status back in VirtualSutra, via POST /webhooks/github/issues.

  • Issue reopened → the linked finding's status is set to open.
  • Issue closed → the linked finding's status is set to resolved.

Requirements and safeguards:

  • The webhook is authenticated via HMAC-SHA256 using the GITHUB_WEBHOOK_SECRET environment variable (configured as the shared secret on the GitHub webhook itself). This is a separate secret from the per-installation webhook_secret used by the PR-check integration under Settings > Integrations.
  • If GITHUB_WEBHOOK_SECRET is unset, the endpoint runs in a permissive dev-mode for reopened events only (an ERROR is logged to surface the misconfiguration) - but a closed event is refused outright when the secret isn't configured, since re-resolving a finding suppresses its visibility and that direction must never run unauthenticated.
  • The finding to update is looked up by its stored external_issue_url, which must match the repository.full_name + issue.number carried in the same signed payload - this stops a payload from naming one repo while pointing at an issue URL belonging to a different one.
  • Jira has no equivalent inbound webhook in this build - closing/reopening a Jira issue does not currently sync back to the finding's status.

SLA-breach escalation

Independent of any new scan, an hourly scheduled job (SLA_BREACH_CHECK_ENABLED, default on; interval via SLA_BREACH_CHECK_INTERVAL_HOURS, default 1 hour) scans every org for open, unmuted findings older than that org's SLA threshold for their severity, and emits an escalated_finding event (escalation_reason: "sla_breach") for each one - which Ticket Sync picks up the same way as a diff-driven escalation.

Default thresholds (hours), overridable per-org:

Severity Default threshold Org override key
Critical 48h (2 days) critical_hours
High 168h (7 days) high_hours
Medium 720h (30 days) medium_hours
Low (untracked - no SLA) -

Setting an override to 0 or a non-positive number disables SLA tracking for that severity. Each org's breach check is isolated - a failure processing one org doesn't stop the hourly cycle from checking the rest. Each finding/severity breach is deduplicated via a Redis key so the same breach doesn't re-fire every hour within the same threshold window.


Environment variables reference

Variable Purpose
TICKET_SYNC_ENABLED Shown as ticket sync's enabled status in GET /notifications/channels. It does not turn ticket sync on or off; each schedule's own ticket_sync.enabled does
TICKET_SYNC_PROVIDER Service-level default provider (jira / github_issues)
TICKET_SYNC_PROJECT_KEY Not a default: a schedule must set project_key itself. Under SaaS, it (with JIRA_PROJECT_KEY) names the only projects on JIRA_DOMAIN the deployment's credential may be used for. Self-hosted it narrows nothing: every project on JIRA_DOMAIN is allowed
TICKET_SYNC_REPO Not a default: a schedule must set repo itself. A GitHub owner/repo the deployment's credential may be used for
TICKET_SYNC_ALLOWED_TARGETS Further targets the deployment's Jira/GitHub credential may be sent to. Comma-separated, self-hosted only (ignored under SaaS): jira:<site> (the bare site name, e.g. acme for acme.atlassian.net), jira:<site>/<PROJECT>, github:<owner>/<repo>, github:<owner>/*. A malformed entry is ignored, with no error
TICKET_SYNC_SEVERITY_THRESHOLD Service-level default severity threshold (default medium)
JIRA_DOMAIN, JIRA_EMAIL, JIRA_API_TOKEN Shared Jira credentials (also used by the separate Jira auto-create channel)
GITHUB_ISSUES_TOKEN Shared GitHub token (also used by the separate GitHub Issues auto-create channel)
GITHUB_WEBHOOK_SECRET HMAC secret for the inbound /webhooks/github/issues bidirectional-sync endpoint
SLA_BREACH_CHECK_ENABLED Enable/disable the hourly SLA-breach scheduler (default true)
SLA_BREACH_CHECK_INTERVAL_HOURS How often the SLA-breach job runs (default 1)

Per-schedule ticket_sync config values (set in the schedule's config JSON) always take precedence over these service-level values. The credential env vars exist as fallbacks so you don't have to repeat credentials on every schedule, but the deployment's credential is only sent to a target on its allowlist. Self-hosted, that is any project on JIRA_DOMAIN, the repos in TICKET_SYNC_REPO / GITHUB_ISSUES_REPO, and TICKET_SYNC_ALLOWED_TARGETS. Under SaaS, it is only the projects in TICKET_SYNC_PROJECT_KEY / JIRA_PROJECT_KEY on JIRA_DOMAIN, plus those repos. A schedule that names any other target must carry its own credential (platform#472); under SaaS, such a schedule is refused when it is saved.