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
This commit is contained in:
1
.kiro/specs/flexible-jira-ticket-creation/.config.kiro
Normal file
1
.kiro/specs/flexible-jira-ticket-creation/.config.kiro
Normal file
@@ -0,0 +1 @@
|
||||
{"specId": "244dcf03-8538-4bc6-b47a-dde02022e4ab", "workflowType": "requirements-first", "specType": "feature"}
|
||||
370
.kiro/specs/flexible-jira-ticket-creation/design.md
Normal file
370
.kiro/specs/flexible-jira-ticket-creation/design.md
Normal file
@@ -0,0 +1,370 @@
|
||||
# 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, 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):**
|
||||
|
||||
```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 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_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'`
|
||||
117
.kiro/specs/flexible-jira-ticket-creation/requirements.md
Normal file
117
.kiro/specs/flexible-jira-ticket-creation/requirements.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Requirements Document
|
||||
|
||||
## Introduction
|
||||
|
||||
The STEAM Security Dashboard's Jira ticket creation flow currently requires a CVE ID and Vendor for every ticket. This locks the workflow to vulnerability-driven contexts only. In practice, the team creates Jira work items from multiple sources — Archer risk acceptance requests, email-driven tasks, and Ivanti queue items — that may not relate to a specific CVE. This feature makes CVE ID and Vendor optional, adds a source context field to track where a ticket originated, and exposes a "Create Jira Ticket" action from the Ivanti queue and Archer ticket views.
|
||||
|
||||
## Glossary
|
||||
|
||||
- **Ticket_Creation_Service**: The backend endpoint (`POST /api/jira-tickets/create-in-jira`) responsible for creating issues in Jira and storing the local record.
|
||||
- **Jira_Tickets_Table**: The PostgreSQL `jira_tickets` table storing local records of Jira issues.
|
||||
- **Creation_Modal**: The frontend modal dialog used to create a new Jira ticket.
|
||||
- **Source_Context**: A metadata field indicating where a Jira ticket originated (e.g., `cve`, `archer`, `ivanti_queue`, `email`, `manual`).
|
||||
- **Ivanti_Queue_Page**: The frontend page where users work through their Ivanti finding queue items.
|
||||
- **Archer_Detail_View**: The frontend view displaying Archer ticket details.
|
||||
- **Dashboard**: The STEAM Security Dashboard application.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement 1: Make CVE ID Optional for Ticket Creation
|
||||
|
||||
**User Story:** As a security analyst, I want to create Jira tickets without providing a CVE ID, so that I can track work items that originate from non-vulnerability contexts.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a create-ticket request is submitted without a cve_id field or with cve_id set to null, THE Ticket_Creation_Service SHALL accept the request, create the Jira issue, store the local ticket record with a NULL cve_id, and return an HTTP 201 response.
|
||||
2. WHEN a create-ticket request includes a cve_id matching the pattern CVE-YYYY-NNNN+ (four-digit year, four or more digit sequence number), THE Ticket_Creation_Service SHALL store the cve_id on the local ticket record.
|
||||
3. IF a create-ticket request includes a cve_id that is a non-empty string not matching the pattern CVE-YYYY-NNNN+ (four-digit year, four or more digit sequence number), THEN THE Ticket_Creation_Service SHALL reject the request with an HTTP 400 response and an error message indicating the CVE ID format is invalid.
|
||||
4. IF a create-ticket request includes a cve_id that is an empty string, THEN THE Ticket_Creation_Service SHALL treat it as absent (NULL) and accept the request.
|
||||
5. THE Jira_Tickets_Table SHALL allow NULL values in the cve_id column.
|
||||
|
||||
### Requirement 2: Make Vendor Optional for Ticket Creation
|
||||
|
||||
**User Story:** As a security analyst, I want to create Jira tickets without providing a Vendor, so that I can create work items for tasks that are not vendor-specific.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHEN a create-ticket request is submitted with the vendor field absent, null, or an empty string, THE Ticket_Creation_Service SHALL accept the request and create the Jira issue with a NULL vendor value stored in the local ticket record.
|
||||
2. WHEN a create-ticket request includes a vendor that is a non-empty string of 1 to 200 characters (after trimming whitespace), THE Ticket_Creation_Service SHALL store the trimmed vendor value on the local ticket record.
|
||||
3. IF a create-ticket request includes a vendor that, after trimming, is longer than 200 characters, THEN THE Ticket_Creation_Service SHALL reject the request with a validation error indicating the vendor exceeds the maximum length.
|
||||
4. THE Jira_Tickets_Table SHALL allow NULL values in the vendor column.
|
||||
5. WHEN a create-ticket request is submitted without a vendor, THE Ticket_Creation_Service SHALL NOT send the vendor value to the Jira API and SHALL still create the Jira issue successfully.
|
||||
|
||||
### Requirement 3: Add Source Context Tracking
|
||||
|
||||
**User Story:** As a security analyst, I want each Jira ticket to record where it was created from, so that I can understand the origin of work items when reviewing the ticket list.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Jira_Tickets_Table SHALL include a source_context column that accepts one of: `cve`, `archer`, `ivanti_queue`, `email`, `manual`.
|
||||
2. WHEN a create-ticket request does not include a source_context value, THE Ticket_Creation_Service SHALL default source_context to `manual`.
|
||||
3. IF a create-ticket request includes a source_context value not in the allowed set, THEN THE Ticket_Creation_Service SHALL reject the request with an HTTP 400 response containing an error message indicating the provided value is not in the allowed set.
|
||||
4. THE Ticket_Creation_Service SHALL store the source_context value on the local ticket record and return it in all GET responses that include ticket data.
|
||||
5. WHEN the source_context column is added, THE Migration SHALL set existing rows to `manual` so that no ticket has a null source_context value.
|
||||
6. IF an update-ticket request includes a source_context value, THEN THE Ticket_Creation_Service SHALL reject the change with an HTTP 400 response containing an error message indicating that source_context is immutable after creation.
|
||||
|
||||
### Requirement 4: Update the Creation Modal for Optional Fields
|
||||
|
||||
**User Story:** As a security analyst, I want the Create Jira Ticket modal to clearly indicate that CVE ID and Vendor are optional, so that I am not blocked from creating tickets without them.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Creation_Modal SHALL display CVE ID and Vendor fields each with a label containing the text "(optional)" and placeholder text indicating example input rather than a required value.
|
||||
2. THE Creation_Modal SHALL allow submission when CVE ID and Vendor are both empty.
|
||||
3. THE Creation_Modal SHALL include a Source Context selector with the options: CVE, Archer Request, Ivanti Queue, Email, Manual, and no option pre-selected by default.
|
||||
4. WHEN the user selects a source context, THE Creation_Modal SHALL send the selected option's corresponding value (`cve`, `archer`, `ivanti_queue`, `email`, `manual`) in the create request payload.
|
||||
5. THE Creation_Modal SHALL require Summary as a mandatory field with a maximum length of 255 characters.
|
||||
6. IF the user attempts to submit with an empty Summary, THEN THE Creation_Modal SHALL prevent submission and display an inline error message indicating that Summary is required.
|
||||
7. IF the user does not select a source context, THEN THE Creation_Modal SHALL omit the source_context field from the create request payload, allowing the backend to default to `manual`.
|
||||
|
||||
### Requirement 5: Create Jira Tickets from Ivanti Queue Context
|
||||
|
||||
**User Story:** As a security analyst, I want to create a Jira ticket directly from an Ivanti queue item, so that I can escalate findings to Jira without navigating away from my workflow.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHILE viewing an Ivanti queue item, THE Ivanti_Queue_Page SHALL display a "Create Jira Ticket" action.
|
||||
2. WHEN the user activates the "Create Jira Ticket" action on a queue item, THE Ivanti_Queue_Page SHALL open the Creation_Modal with the Summary field pre-populated with the queue item's finding_title (truncated to 255 characters) and source_context set to `ivanti_queue`.
|
||||
3. WHEN the queue item has a non-empty cves_json array, THE Creation_Modal SHALL pre-populate the CVE ID field with the first element of the cves_json array.
|
||||
4. IF the queue item has an empty or null cves_json value, THEN THE Creation_Modal SHALL leave the CVE ID field blank.
|
||||
5. WHEN the queue item has a vendor value, THE Creation_Modal SHALL pre-populate the Vendor field with that value.
|
||||
6. IF the Jira API is unavailable or returns an error when the user submits the Creation_Modal, THEN THE system SHALL display an error indication describing the failure and preserve the form field values so the user can retry without re-entering data.
|
||||
|
||||
### Requirement 6: Create Jira Tickets from Archer Ticket Context
|
||||
|
||||
**User Story:** As a security analyst, I want to create a Jira ticket from an Archer ticket view, so that I can create related Jira work items for risk acceptance exceptions.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. WHILE viewing Archer ticket details, IF the user has editor or admin role, THEN THE Archer_Detail_View SHALL display a "Create Jira Ticket" action.
|
||||
2. WHEN the user activates the "Create Jira Ticket" action on an Archer ticket, THE Archer_Detail_View SHALL open the Creation_Modal with the summary field pre-populated with the Archer ticket exc_number (e.g., "EXC-1234") and source_context set to `archer`.
|
||||
3. IF the Archer ticket has an associated CVE ID, THEN THE Creation_Modal SHALL pre-populate the CVE ID field with that value.
|
||||
4. IF the Archer ticket has an associated vendor, THEN THE Creation_Modal SHALL pre-populate the Vendor field with that value.
|
||||
5. IF the Jira ticket creation API call fails, THEN THE Creation_Modal SHALL display an error message indicating the failure reason and SHALL retain all user-entered form data.
|
||||
|
||||
### Requirement 7: Database Migration for Schema Changes
|
||||
|
||||
**User Story:** As a database administrator, I want the schema changes applied via a migration, so that existing deployments can upgrade without data loss.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Dashboard SHALL include a migration that alters the jira_tickets table to allow NULL in the cve_id column by dropping the NOT NULL constraint.
|
||||
2. THE Dashboard SHALL include a migration that alters the jira_tickets table to allow NULL in the vendor column by dropping the NOT NULL constraint.
|
||||
3. THE Dashboard SHALL include a migration that adds a source_context TEXT column to the jira_tickets table with a default value of `manual`, where existing rows receive the default value upon column addition.
|
||||
4. WHEN the migration runs on a database with existing jira_tickets rows, THE migration SHALL preserve all existing row values in the cve_id, vendor, ticket_key, url, summary, status, created_at, and updated_at columns without modification.
|
||||
5. THE migration SHALL be idempotent — running it multiple times SHALL produce the same schema state and data as running it once, without raising errors on subsequent executions.
|
||||
6. IF the jira_tickets table does not exist when the migration runs, THEN THE migration SHALL exit with an error message indicating the table is missing and no schema changes were applied.
|
||||
|
||||
### Requirement 8: Display Source Context in Ticket List
|
||||
|
||||
**User Story:** As a security analyst, I want to see the source context of each ticket in the Jira tickets list, so that I can quickly identify where work items originated.
|
||||
|
||||
#### Acceptance Criteria
|
||||
|
||||
1. THE Dashboard SHALL display the source_context value as a badge on each ticket row in the Jira tickets list view, positioned as a column between the VENDOR and SUMMARY columns.
|
||||
2. IF a ticket has no source_context value (null or empty string), THEN THE Dashboard SHALL display "CVE" as the default badge text since all legacy tickets were CVE-linked.
|
||||
3. THE Dashboard SHALL provide a dropdown filter for source_context values, consistent with the existing status filter dropdown, that includes an "All" option showing all tickets and one option per distinct source_context value present in the ticket data.
|
||||
4. WHEN the user enters a search term in the ticket search input, THE Dashboard SHALL include the source_context value in the searchable fields alongside ticket_key, cve_id, vendor, and summary.
|
||||
139
.kiro/specs/flexible-jira-ticket-creation/tasks.md
Normal file
139
.kiro/specs/flexible-jira-ticket-creation/tasks.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# Implementation Plan: Flexible Jira Ticket Creation
|
||||
|
||||
## Overview
|
||||
|
||||
This plan implements flexible Jira ticket creation by making CVE ID and Vendor optional, adding source_context tracking, updating the creation modal, exposing "Create Jira Ticket" actions from Ivanti queue and Archer detail views, and updating the ticket list with source context display and filtering. The implementation proceeds from database migration → backend validation → frontend modal updates → integration points → list view enhancements.
|
||||
|
||||
## Tasks
|
||||
|
||||
- [x] 1. Database migration for schema changes
|
||||
- [x] 1.1 Create migration file `backend/migrations/add_flexible_jira_ticket_creation.js`
|
||||
- Drop NOT NULL constraint on `cve_id` column
|
||||
- Drop NOT NULL constraint on `vendor` column
|
||||
- Add `source_context TEXT DEFAULT 'manual'` column with `IF NOT EXISTS`
|
||||
- Add CHECK constraint for allowed source_context values (`cve`, `archer`, `ivanti_queue`, `email`, `manual`) with idempotent guard
|
||||
- Backfill existing rows with `source_context = 'manual'` where NULL
|
||||
- Add index on `source_context` column
|
||||
- Verify `jira_tickets` table exists before proceeding; exit with error if missing
|
||||
- Ensure full idempotency — safe to run multiple times
|
||||
- _Requirements: 1.5, 2.4, 3.1, 3.5, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6_
|
||||
|
||||
- [x] 2. Update backend validation and creation endpoint
|
||||
- [x] 2.1 Update `POST /api/jira-tickets/create-in-jira` validation logic in `backend/routes/jiraTickets.js`
|
||||
- Make `cve_id` optional: accept absent, null, or empty string as NULL; validate format `CVE-YYYY-NNNN+` only when non-empty
|
||||
- Make `vendor` optional: accept absent, null, empty, or whitespace-only as NULL; trim and validate max 200 chars when non-empty
|
||||
- Add `source_context` validation: must be in allowed set if provided, default to `manual` if absent
|
||||
- Update INSERT query to include `source_context` column
|
||||
- Update 201 response to include `source_context` in returned JSON
|
||||
- Update audit log details to include `source_context`
|
||||
- _Requirements: 1.1, 1.2, 1.3, 1.4, 2.1, 2.2, 2.3, 2.5, 3.2, 3.3, 3.4_
|
||||
|
||||
- [x] 2.2 Update `PUT /api/jira-tickets/:id` to reject source_context changes
|
||||
- If request body contains `source_context` field, return 400 with "source_context is immutable after creation"
|
||||
- _Requirements: 3.6_
|
||||
|
||||
- [x] 2.3 Update `GET /api/jira-tickets` to include source_context in response
|
||||
- Ensure `source_context` is included in SELECT query and returned in ticket objects
|
||||
- _Requirements: 3.4_
|
||||
|
||||
- [ ]* 2.4 Write property tests for CVE ID validation (Property 1 and Property 2)
|
||||
- **Property 1: CVE ID validation and storage** — For any payload with absent/null/empty cve_id, service accepts and stores NULL; for valid CVE format, stores exact value
|
||||
- **Property 2: Invalid CVE ID rejection** — For any non-empty string not matching `CVE-YYYY-NNNN+`, service rejects with 400
|
||||
- **Validates: Requirements 1.1, 1.2, 1.3, 1.4**
|
||||
- File: `backend/__tests__/jira-flexible-cve-validation.property.test.js`
|
||||
|
||||
- [ ]* 2.5 Write property tests for Vendor validation (Property 3 and Property 4)
|
||||
- **Property 3: Vendor validation and storage** — For any payload with absent/null/empty/whitespace vendor, stores NULL; for 1–200 char string after trim, stores trimmed value
|
||||
- **Property 4: Over-length vendor rejection** — For any string exceeding 200 chars after trim, service rejects with validation error
|
||||
- **Validates: Requirements 2.1, 2.2, 2.3**
|
||||
- File: `backend/__tests__/jira-flexible-vendor-validation.property.test.js`
|
||||
|
||||
- [ ]* 2.6 Write property tests for source_context validation (Property 5, Property 6, Property 7)
|
||||
- **Property 5: Invalid source_context rejection** — For any string not in allowed set, service rejects with 400
|
||||
- **Property 6: source_context round-trip persistence** — For any valid source_context, creating then fetching returns same value
|
||||
- **Property 7: source_context immutability** — For any existing ticket, update with source_context field is rejected with 400
|
||||
- **Validates: Requirements 3.3, 3.4, 3.6**
|
||||
- File: `backend/__tests__/jira-flexible-source-context.property.test.js`
|
||||
|
||||
- [x] 3. Checkpoint - Ensure all tests pass
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
- [x] 4. Update frontend Creation Modal for optional fields
|
||||
- [x] 4.1 Update the create Jira ticket modal in `frontend/src/components/pages/JiraPage.js`
|
||||
- Change CVE ID label to "CVE ID (optional)" with placeholder "e.g. CVE-2024-12345"
|
||||
- Change Vendor label to "Vendor (optional)" with placeholder "e.g. Microsoft"
|
||||
- Add Source Context dropdown with options: CVE → `cve`, Archer Request → `archer`, Ivanti Queue → `ivanti_queue`, Email → `email`, Manual → `manual`; no default selection
|
||||
- Allow form submission when CVE ID and Vendor are both empty
|
||||
- Keep Summary as required with inline error on empty submit (max 255 chars)
|
||||
- Send `source_context` in payload only when selected; omit if no selection (backend defaults to `manual`)
|
||||
- Support pre-populated field values and locked source_context (read-only when set externally)
|
||||
- Preserve form field values on API error for retry
|
||||
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 5.6, 6.5_
|
||||
|
||||
- [x] 5. Add "Create Jira Ticket" action from Ivanti Queue
|
||||
- [x] 5.1 Add "Create Jira Ticket" button to Ivanti queue items in `frontend/src/components/pages/IvantiTodoQueuePage.js`
|
||||
- Add button/action to each queue item row
|
||||
- On click, open Creation Modal with: summary pre-populated from `finding_title` (truncated to 255 chars), source_context locked to `ivanti_queue`, cve_id from first element of `cves_json` (if non-empty), vendor from queue item's vendor (if present)
|
||||
- Leave CVE ID blank if `cves_json` is empty or null
|
||||
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5_
|
||||
|
||||
- [ ]* 5.2 Write property test for summary truncation (Property 8)
|
||||
- **Property 8: Summary pre-population truncation from Ivanti queue** — For any finding_title of arbitrary length, pre-populated summary is at most 255 chars and equals first 255 chars of finding_title
|
||||
- **Validates: Requirements 5.2**
|
||||
- File: `backend/__tests__/jira-flexible-summary-truncation.property.test.js`
|
||||
|
||||
- [x] 6. Add "Create Jira Ticket" action from Archer Detail View
|
||||
- [x] 6.1 Add "Create Jira Ticket" button to Archer ticket detail view in `frontend/src/components/pages/ArcherPage.js`
|
||||
- Show button only for users with editor or admin role
|
||||
- On click, open Creation Modal with: summary pre-populated with `exc_number` (e.g., "EXC-1234"), source_context locked to `archer`, cve_id from Archer ticket's cve_id (if present), vendor from Archer ticket's vendor (if present)
|
||||
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
|
||||
|
||||
- [x] 7. Update ticket list with source context display and filtering
|
||||
- [x] 7.1 Add source context badge column to ticket list in `frontend/src/components/pages/JiraPage.js`
|
||||
- Add "Source" column between Vendor and Summary columns
|
||||
- Display color-coded badge per source_context value: cve → blue (#0EA5E9), archer → purple (#8B5CF6), ivanti_queue → amber (#F59E0B), email → green (#10B981), manual → gray (#94A3B8)
|
||||
- Display "CVE" badge (blue) for null/empty source_context (legacy tickets)
|
||||
- _Requirements: 8.1, 8.2_
|
||||
|
||||
- [x] 7.2 Add source context dropdown filter to ticket list
|
||||
- Add dropdown filter consistent with existing status filter pattern
|
||||
- Include "All" option showing all tickets plus one option per distinct source_context value
|
||||
- _Requirements: 8.3_
|
||||
|
||||
- [x] 7.3 Include source_context in ticket search
|
||||
- Add source_context to the set of searchable fields alongside ticket_key, cve_id, vendor, and summary
|
||||
- _Requirements: 8.4_
|
||||
|
||||
- [ ]* 7.4 Write property test for search includes source_context (Property 9)
|
||||
- **Property 9: Search includes source_context** — For any ticket whose source_context contains the search term as a substring, that ticket appears in filtered results
|
||||
- **Validates: Requirements 8.4**
|
||||
- File: `backend/__tests__/jira-flexible-search-source-context.property.test.js`
|
||||
|
||||
- [x] 8. Final checkpoint - Ensure all tests pass
|
||||
- Ensure all tests pass, ask the user if questions arise.
|
||||
|
||||
## Notes
|
||||
|
||||
- Tasks marked with `*` are optional and can be skipped for faster MVP
|
||||
- Each task references specific requirements for traceability
|
||||
- Checkpoints ensure incremental validation
|
||||
- Property tests validate universal correctness properties from the design document
|
||||
- Unit tests validate specific examples and edge cases
|
||||
- The migration must run before backend changes are tested against a real database
|
||||
- The Creation Modal is shared across all three entry points (Jira page, Ivanti queue, Archer detail) — task 4.1 builds the reusable foundation, tasks 5.1 and 6.1 wire it from their respective contexts
|
||||
|
||||
## Task Dependency Graph
|
||||
|
||||
```json
|
||||
{
|
||||
"waves": [
|
||||
{ "id": 0, "tasks": ["1.1"] },
|
||||
{ "id": 1, "tasks": ["2.1", "2.2", "2.3"] },
|
||||
{ "id": 2, "tasks": ["2.4", "2.5", "2.6"] },
|
||||
{ "id": 3, "tasks": ["4.1"] },
|
||||
{ "id": 4, "tasks": ["5.1", "6.1"] },
|
||||
{ "id": 5, "tasks": ["5.2", "7.1", "7.2", "7.3"] },
|
||||
{ "id": 6, "tasks": ["7.4"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user