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
16 KiB
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, 1–200 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_contextfield
Source Context Options Mapping:
| Display Label | API Value |
|---|---|
| CVE | cve |
| Archer Request | archer |
| Ivanti Queue | ivanti_queue |
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:
- Opens the Creation Modal
- Pre-populates
summarywithfinding_title(truncated to 255 chars) - Pre-populates
cve_idwith first element ofcves_jsonarray (if non-empty) - Pre-populates
vendorwith queue item'svendorvalue (if present) - Sets
source_contexttoivanti_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:
- Opens the Creation Modal
- Pre-populates
summarywithexc_number(e.g., "EXC-1234") - Pre-populates
cve_idwith Archer ticket'scve_id(if present) - Pre-populates
vendorwith Archer ticket'svendor(if present) - Sets
source_contexttoarcher(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_contextin 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_contexthas a CHECK constraint limiting values to('cve', 'archer', 'ivanti_queue', 'email', 'manual')cve_idallows NULL (NOT NULL constraint dropped)vendorallows 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 1–200 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 1–7 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_ticketsrows, preserving all data - Migration is idempotent (running twice produces same result)
- Migration fails gracefully when
jira_ticketstable 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'