Files
cve-dashboard/.kiro/specs/flexible-jira-ticket-creation/design.md
Jordan Ramos a61d254ff9 Sync .kiro/ from master — v2.2.0 release batch
New specs: archer-template-library, ccp-metrics-view-restructure,
compliance-list-stale-after-sidebar-edit, compliance-metric-estimated-resolution-date,
compliance-remediation-display-fix, flexible-jira-ticket-creation,
forecast-burndown-chart, granite-loader-export, ivanti-queue-clear-completed-fix,
multi-item-jira-ticket, queue-collapsible-sections, vendor-issue-type-dropdown

New steering: archer-template-gen.md

Updated: migration-registration-check hook, remediation-plan-history spec,
gitlab-workflow, tech, versioning steering files
2026-06-04 11:27:31 -06:00

16 KiB
Raw Blame History

Design Document: Flexible Jira Ticket Creation

Overview

This feature decouples Jira ticket creation from the CVE-only workflow by making CVE ID and Vendor optional, adding a source_context field to track ticket origin, and exposing "Create Jira Ticket" actions from the Ivanti queue and Archer detail views. The changes span a database migration, backend validation updates, and frontend modal/list enhancements.

The design preserves backward compatibility — existing tickets retain their CVE ID and Vendor values, and the new source_context column defaults to manual for legacy rows.

Architecture

flowchart TD
    subgraph Frontend
        JiraPage[JiraPage.js<br/>Creation Modal + Ticket List]
        IvantiQueue[Ivanti Queue Page<br/>Create Jira Ticket action]
        ArcherView[App.js Archer Detail<br/>Create Jira Ticket action]
    end

    subgraph Backend
        Route[POST /api/jira-tickets/create-in-jira]
        Validation[Input Validation Layer]
        JiraAPI[jiraApi.js → Jira REST API]
        DB[(PostgreSQL<br/>jira_tickets table)]
    end

    IvantiQueue -->|opens modal with pre-populated fields| JiraPage
    ArcherView -->|opens modal with pre-populated fields| JiraPage
    JiraPage -->|POST payload| Route
    Route --> Validation
    Validation -->|valid| JiraAPI
    JiraAPI -->|issue created| DB
    Validation -->|invalid| Route
    Route -->|400/201| JiraPage

The modal component is reused across all three entry points (Jira page, Ivanti queue, Archer detail). Each entry point passes pre-populated field values and a locked source_context value to the modal.

Components and Interfaces

Backend: POST /api/jira-tickets/create-in-jira (Updated)

Request Body:

Field Type Required Validation
summary string Yes Non-empty after trim, max 255 chars
cve_id string | null No If present and non-empty, must match CVE-YYYY-NNNN+. Empty string treated as null.
vendor string | null No If present and non-empty, 1200 chars after trim. Empty string treated as null.
source_context string | null No Must be one of: cve, archer, ivanti_queue, email, manual. Defaults to manual if absent.
description string | null No Free text, passed to Jira issue body
project_key string | null No Falls back to JIRA_PROJECT_KEY env var
issue_type string | null No Falls back to JIRA_ISSUE_TYPE env var

Response (201):

{
  "id": 42,
  "ticket_key": "VULN-789",
  "jira_url": "https://jira.example.com/browse/VULN-789",
  "source_context": "ivanti_queue",
  "message": "Jira issue created and linked successfully"
}

Validation Logic (pseudocode):

if cve_id is empty string → treat as null
if cve_id is non-null and non-empty:
    if not matching /^CVE-\d{4}-\d{4,}$/ → 400
if vendor is empty string or whitespace-only → treat as null
if vendor is non-null and non-empty:
    trim whitespace
    if length > 200 → 400
if source_context is present:
    if not in ALLOWED_SET → 400
else:
    source_context = 'manual'
if summary is empty or > 255 chars → 400

Backend: PUT /api/jira-tickets/:id (Updated)

The update endpoint rejects any attempt to change source_context:

if request body contains source_context field → 400 "source_context is immutable after creation"

Backend: GET /api/jira-tickets (Updated)

Response now includes source_context on each ticket object. No other changes to the GET endpoint.

Frontend: Creation Modal (Updated)

The existing showCreateJira modal in JiraPage.js is updated:

  • CVE ID label: "CVE ID (optional)" with placeholder "e.g. CVE-2024-12345"
  • Vendor label: "Vendor (optional)" with placeholder "e.g. Microsoft"
  • New Source Context dropdown: options map display labels to API values
  • Summary remains required with inline validation error on empty submit
  • Form state extended with source_context field

Source Context Options Mapping:

Display Label API Value
CVE cve
Archer Request archer
Ivanti Queue ivanti_queue
Email email
Manual manual

When opened from Ivanti queue or Archer context, the source_context dropdown is pre-selected and read-only.

Frontend: Ivanti Queue Integration

A "Create Jira Ticket" button is added to each queue item row (or action menu). When activated:

  1. Opens the Creation Modal
  2. Pre-populates summary with finding_title (truncated to 255 chars)
  3. Pre-populates cve_id with first element of cves_json array (if non-empty)
  4. Pre-populates vendor with queue item's vendor value (if present)
  5. Sets source_context to ivanti_queue (locked)

Frontend: Archer Detail Integration

A "Create Jira Ticket" button is added to the Archer ticket detail view (visible to editor/admin roles). When activated:

  1. Opens the Creation Modal
  2. Pre-populates summary with exc_number (e.g., "EXC-1234")
  3. Pre-populates cve_id with Archer ticket's cve_id (if present)
  4. Pre-populates vendor with Archer ticket's vendor (if present)
  5. Sets source_context to archer (locked)

Frontend: Ticket List Updates

The ticket list table gains:

  • A new "Source" column between Vendor and Summary, displaying a color-coded badge
  • A source_context dropdown filter (matching the existing status filter pattern)
  • Search includes source_context in the filterable fields

Badge Color Mapping:

Source Context Badge Color
cve #0EA5E9 (blue)
archer #8B5CF6 (purple)
ivanti_queue #F59E0B (amber)
email #10B981 (green)
manual #94A3B8 (gray)

Null/empty source_context displays as "CVE" badge (blue) for backward compatibility with legacy tickets.

Data Models

jira_tickets Table (Updated Schema)

CREATE TABLE IF NOT EXISTS jira_tickets (
    id SERIAL PRIMARY KEY,
    cve_id TEXT,                    -- Changed: NULL allowed
    vendor TEXT,                    -- Changed: NULL allowed
    ticket_key TEXT NOT NULL,
    url TEXT,
    summary TEXT,
    status TEXT DEFAULT 'Open' CHECK (status IN ('Open', 'In Progress', 'Closed')),
    source_context TEXT DEFAULT 'manual',  -- New column
    jira_id TEXT,
    jira_status TEXT,
    last_synced_at TIMESTAMPTZ,
    created_by INTEGER REFERENCES users(id),
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW()
);

Constraints:

  • source_context has a CHECK constraint limiting values to ('cve', 'archer', 'ivanti_queue', 'email', 'manual')
  • cve_id allows NULL (NOT NULL constraint dropped)
  • vendor allows NULL (NOT NULL constraint dropped)

Migration: add_flexible_jira_ticket_creation.js

const pool = require('../db');

async function run() {
    console.log('Starting flexible Jira ticket creation migration...');

    // Verify table exists
    const { rows } = await pool.query(`
        SELECT 1 FROM information_schema.tables
        WHERE table_name = 'jira_tickets'
    `);
    if (rows.length === 0) {
        console.error('✗ jira_tickets table does not exist. Cannot proceed.');
        process.exit(1);
    }

    // Drop NOT NULL on cve_id
    await pool.query(`ALTER TABLE jira_tickets ALTER COLUMN cve_id DROP NOT NULL`);
    console.log('✓ cve_id NOT NULL constraint dropped (or was already nullable)');

    // Drop NOT NULL on vendor
    await pool.query(`ALTER TABLE jira_tickets ALTER COLUMN vendor DROP NOT NULL`);
    console.log('✓ vendor NOT NULL constraint dropped (or was already nullable)');

    // Add source_context column with default
    await pool.query(`
        ALTER TABLE jira_tickets
        ADD COLUMN IF NOT EXISTS source_context TEXT DEFAULT 'manual'
    `);
    console.log('✓ source_context column added (or already exists)');

    // Add CHECK constraint (idempotent via IF NOT EXISTS pattern)
    await pool.query(`
        DO $$
        BEGIN
            IF NOT EXISTS (
                SELECT 1 FROM pg_constraint WHERE conname = 'jira_tickets_source_context_check'
            ) THEN
                ALTER TABLE jira_tickets
                ADD CONSTRAINT jira_tickets_source_context_check
                CHECK (source_context IN ('cve', 'archer', 'ivanti_queue', 'email', 'manual'));
            END IF;
        END $$;
    `);
    console.log('✓ source_context CHECK constraint added (or already exists)');

    // Backfill existing rows (DEFAULT handles this, but explicit for clarity)
    await pool.query(`
        UPDATE jira_tickets SET source_context = 'manual' WHERE source_context IS NULL
    `);
    console.log('✓ Existing rows backfilled with source_context = manual');

    // Index for filtering
    await pool.query(`
        CREATE INDEX IF NOT EXISTS idx_jira_tickets_source_context
        ON jira_tickets(source_context)
    `);
    console.log('✓ source_context index created (or already exists)');

    console.log('Migration complete.');
    process.exit(0);
}

run().catch(err => {
    console.error('Migration failed:', err.message);
    process.exit(1);
});

Idempotency: ALTER COLUMN DROP NOT NULL is safe to run multiple times (no-op if already nullable). ADD COLUMN IF NOT EXISTS and CREATE INDEX IF NOT EXISTS are inherently idempotent. The CHECK constraint uses a DO $$ ... IF NOT EXISTS guard.

Correctness Properties

A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.

Property 1: CVE ID validation and storage

For any create-ticket request payload, if cve_id is absent, null, or an empty string, the service SHALL accept the request and store NULL for cve_id; if cve_id is a non-empty string matching CVE-YYYY-NNNN+, the service SHALL store that exact value.

Validates: Requirements 1.1, 1.2, 1.4

Property 2: Invalid CVE ID rejection

For any non-empty string that does not match the pattern CVE-YYYY-NNNN+ (four-digit year, four or more digit sequence), the service SHALL reject the create-ticket request with HTTP 400.

Validates: Requirements 1.3

Property 3: Vendor validation and storage

For any create-ticket request payload, if vendor is absent, null, empty, or whitespace-only, the service SHALL store NULL; if vendor is a non-empty string of 1200 characters after trimming, the service SHALL store the trimmed value.

Validates: Requirements 2.1, 2.2

Property 4: Over-length vendor rejection

For any string that, after trimming whitespace, exceeds 200 characters, the service SHALL reject the create-ticket request with a validation error.

Validates: Requirements 2.3

Property 5: Invalid source_context rejection

For any string not in the set {cve, archer, ivanti_queue, email, manual}, the service SHALL reject the create-ticket request with HTTP 400 when that string is provided as source_context.

Validates: Requirements 3.3

Property 6: source_context round-trip persistence

For any valid source_context value in {cve, archer, ivanti_queue, email, manual}, creating a ticket with that value and then fetching the ticket via GET SHALL return the same source_context value.

Validates: Requirements 3.4

Property 7: source_context immutability

For any existing ticket and any source_context value (valid or invalid), an update request that includes a source_context field SHALL be rejected with HTTP 400.

Validates: Requirements 3.6

Property 8: Summary pre-population truncation from Ivanti queue

For any Ivanti queue item with a finding_title of arbitrary length, the pre-populated summary in the Creation Modal SHALL be at most 255 characters and SHALL equal the first 255 characters of finding_title.

Validates: Requirements 5.2

Property 9: Search includes source_context

For any ticket whose source_context value contains the search term as a substring, that ticket SHALL appear in the filtered results when the user searches by that term.

Validates: Requirements 8.4

Error Handling

Scenario HTTP Status Error Message Client Behavior
Invalid CVE ID format 400 "CVE ID format is invalid. Expected CVE-YYYY-NNNN+." Display inline error, preserve form
Vendor exceeds 200 chars 400 "Vendor exceeds maximum length of 200 characters." Display inline error, preserve form
Invalid source_context 400 "source_context must be one of: cve, archer, ivanti_queue, email, manual." Display inline error, preserve form
source_context update attempt 400 "source_context is immutable after creation." Display inline error
Empty summary 400 "Summary is required (max 255 chars)." Prevent submission, show inline error
Jira API unavailable 502 "Failed to create Jira issue." Display error banner, preserve form values for retry
Jira rate limit exceeded 429 "Jira rate limit exceeded. Try again later." Display error banner, preserve form values
Jira API not configured 503 "Jira API is not configured." Display error banner
DB insert fails after Jira creation 207 Warning with Jira key/URL Display warning with Jira link
jira_tickets table missing (migration) Exit code 1 Console error Migration aborts cleanly

Error preservation: When the Jira API call fails or returns an error, the frontend modal retains all user-entered form data so the user can retry without re-entering information. This applies to all three entry points (Jira page, Ivanti queue, Archer detail).

Testing Strategy

Unit Tests (Example-Based)

  • Modal renders with "(optional)" labels and correct placeholders
  • Modal allows submission with empty CVE ID and Vendor
  • Source Context dropdown contains all five options with no default selection
  • Source Context dropdown sends correct API value for each display label
  • Summary validation prevents empty submission with inline error
  • Ivanti queue "Create Jira Ticket" button is visible on queue items
  • Archer detail "Create Jira Ticket" button visible for editor/admin, hidden for viewer
  • Archer pre-populates summary with exc_number
  • Ticket list displays source_context badge between Vendor and Summary columns
  • Null source_context displays "CVE" badge text
  • Source context filter dropdown includes "All" and all distinct values
  • API error preserves form field values in modal

Property-Based Tests

Property-based testing is appropriate for this feature because the backend validation logic operates on a wide input space (arbitrary strings for CVE IDs, vendors, and source_context values) where universal properties must hold.

Library: fast-check (already available in the project's test infrastructure via Jest)

Configuration:

  • Minimum 100 iterations per property test
  • Each test tagged with: Feature: flexible-jira-ticket-creation, Property {N}: {title}

Properties 17 test backend validation logic (pure input → output behavior, mockable Jira API). Property 8 tests frontend pre-population logic (pure string truncation). Property 9 tests frontend filtering logic (pure array filter).

Integration Tests

  • Migration runs successfully on a database with existing jira_tickets rows, preserving all data
  • Migration is idempotent (running twice produces same result)
  • Migration fails gracefully when jira_tickets table doesn't exist
  • End-to-end: create ticket from Ivanti queue context → verify stored with source_context = 'ivanti_queue'
  • End-to-end: create ticket from Archer context → verify stored with source_context = 'archer'