Files
cve-dashboard/.kiro/specs/flexible-jira-ticket-creation/design.md

371 lines
16 KiB
Markdown
Raw Normal View 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
```mermaid
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):**
```json
{
"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)
```sql
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`
```javascript
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'`