Files
cve-dashboard/.kiro/specs/fp-submission-editing/design.md

19 KiB
Raw Blame History

Design Document: FP Submission Editing

Overview

This feature extends the existing FP workflow submission system to support viewing, editing, and resubmitting False Positive submissions. It adds lifecycle status tracking, an edit modal triggered from clickable workflow badges in the Reporting Table and from a submissions list in the Queue Panel, backend endpoints that proxy update/map/attach operations to the Ivanti API, and a submission history audit trail.

The design builds on the existing ivantiFpWorkflow.js route, FpWorkflowModal component, and ivanti_fp_submissions table. It follows the same conventions: factory-pattern Express routes, inline React components with the dark tactical theme, Multer for file uploads, and the ivantiFormPost() / ivantiPost() helpers for Ivanti API calls.

Key Ivanti API endpoints used for editing:

  • POST /workflowBatch/falsePositive/update — update workflow metadata
  • POST /workflowBatch/falsePositive/{uuid}/map — add findings to existing workflow
  • POST /workflowBatch/falsePositive/{uuid}/attach — upload additional attachments

Architecture

sequenceDiagram
    participant U as User
    participant FE as React Frontend
    participant BE as Express Backend
    participant IV as Ivanti API
    participant DB as SQLite

    Note over U,FE: Entry Point A: Clickable Workflow Badge
    U->>FE: Click Reworked/Rejected/Expired badge in Reporting Table
    FE->>FE: Look up FP_Submission by workflow batch ID
    FE->>FE: Open FpEditModal pre-populated with submission data

    Note over U,FE: Entry Point B: Queue Panel Submissions List
    U->>FE: Click submission in Queue Panel submissions list
    FE->>FE: Open FpEditModal pre-populated with submission data

    Note over U,DB: Load Submission Data
    FE->>BE: GET /api/ivanti/fp-submissions
    BE->>DB: SELECT from ivanti_fp_submissions
    DB-->>BE: Submission records
    BE-->>FE: JSON array of submissions

    Note over U,IV: Edit Form Fields
    U->>FE: Modify name/reason/description/expiration, click Save
    FE->>BE: PUT /api/ivanti/fp-submissions/:id
    BE->>BE: Validate input
    BE->>IV: POST /workflowBatch/falsePositive/update
    IV-->>BE: 200 OK
    BE->>DB: UPDATE ivanti_fp_submissions
    BE->>DB: INSERT ivanti_fp_submission_history
    BE->>DB: INSERT audit_log
    BE-->>FE: 200 + updated record

    Note over U,IV: Add Findings
    U->>FE: Select additional FP queue items, click Add
    FE->>BE: POST /api/ivanti/fp-submissions/:id/findings
    BE->>IV: POST /workflowBatch/falsePositive/{uuid}/map
    IV-->>BE: 200 OK
    BE->>DB: UPDATE finding_ids_json
    BE->>DB: UPDATE queue items → complete
    BE->>DB: INSERT history + audit
    BE-->>FE: 200 + updated record

    Note over U,IV: Add Attachments
    U->>FE: Upload files, click Attach
    FE->>BE: POST /api/ivanti/fp-submissions/:id/attachments (multipart)
    loop Each file
        BE->>IV: POST /workflowBatch/falsePositive/{uuid}/attach
        IV-->>BE: 200 OK
    end
    BE->>DB: UPDATE attachment_count, attachment_results_json
    BE->>DB: INSERT history + audit
    BE-->>FE: 200 + attachment results

    Note over U,DB: Status Transition
    U->>FE: Change lifecycle status
    FE->>BE: PATCH /api/ivanti/fp-submissions/:id/status
    BE->>DB: UPDATE lifecycle_status, INSERT history + audit
    BE-->>FE: 200 OK

Components and Interfaces

Backend

Extended Route Module: backend/routes/ivantiFpWorkflow.js

Extends the existing createIvantiFpWorkflowRouter(db, requireAuth) with five new endpoints. All endpoints use requireAuth(db) and requireGroup('Admin', 'Standard_User'), and verify the authenticated user owns the submission (returning 403 otherwise).

Endpoint: GET /api/ivanti/fp-submissions

Returns the authenticated user's FP submission records.

  • Auth: requireAuth(db), any authenticated user (viewers get read-only list)
  • Response:
[
  {
    "id": 1,
    "user_id": 5,
    "username": "jdoe",
    "ivanti_workflow_batch_id": 33418832,
    "ivanti_workflow_batch_uuid": "abc-123-def",
    "workflow_name": "FP - CVE-2024-1234",
    "reason": "Scanner false positive",
    "description": "Confirmed by manual review",
    "expiration_date": "2026-12-31",
    "scope_override": "Authorized",
    "finding_ids_json": "[\"2283734550\",\"2283734551\"]",
    "queue_item_ids_json": "[1,2]",
    "attachment_count": 2,
    "attachment_results_json": "[{\"filename\":\"evidence.pdf\",\"success\":true}]",
    "status": "success",
    "lifecycle_status": "rework",
    "error_message": null,
    "created_at": "2026-04-08T18:16:08",
    "updated_at": "2026-04-10T12:00:00"
  }
]

Endpoint: PUT /api/ivanti/fp-submissions/:id

Updates form fields and proxies to Ivanti update endpoint.

  • Auth: requireAuth(db), requireGroup('Admin', 'Standard_User')
  • Ownership: verified via user_id match
  • Lifecycle guard: rejects if lifecycle_status === 'approved'
  • Request body:
{
  "name": "Updated FP - CVE-2024-1234",
  "reason": "Updated reason",
  "description": "Updated description",
  "expirationDate": "2027-06-01",
  "scopeOverride": "Authorized"
}
  • Validation: same rules as creation form (validateFpWorkflowForm)
  • Ivanti call: POST /client/{clientId}/workflowBatch/falsePositive/update with JSON body containing workflowBatchId and updated fields
  • On success: updates local record, inserts history row, logs audit, sets lifecycle_status to resubmitted if previous status was rejected or rework
  • Response: { success: true, submission: { ...updatedRecord } }

Endpoint: POST /api/ivanti/fp-submissions/:id/findings

Maps additional findings to the existing workflow batch.

  • Auth: requireAuth(db), requireGroup('Admin', 'Standard_User')
  • Ownership: verified
  • Lifecycle guard: rejects if lifecycle_status === 'approved'
  • Request body:
{
  "findingIds": ["2283734552", "2283734553"],
  "queueItemIds": [3, 4]
}
  • Validates queue items belong to user, are FP type, and pending
  • Ivanti call: POST /client/{clientId}/workflowBatch/falsePositive/{uuid}/map with subjectFilterRequest containing the new finding IDs
  • On success: appends new IDs to finding_ids_json, marks queue items complete, inserts history + audit
  • Response: { success: true, addedFindings: [...], queueItemsUpdated: 2 }

Endpoint: POST /api/ivanti/fp-submissions/:id/attachments

Uploads additional files to the existing workflow batch.

  • Auth: requireAuth(db), requireGroup('Admin', 'Standard_User')
  • Content-Type: multipart/form-data (Multer)
  • Ownership: verified
  • Lifecycle guard: rejects if lifecycle_status === 'approved'
  • File constraints: same as creation (10 MB, allowed extensions)
  • Ivanti call: for each file, POST /client/{clientId}/workflowBatch/falsePositive/{uuid}/attach
  • On success: updates attachment_count and attachment_results_json, inserts history + audit
  • Response:
{
  "success": true,
  "attachmentResults": [
    { "filename": "new-evidence.pdf", "success": true },
    { "filename": "screenshot.png", "success": false, "error": "Upload failed" }
  ],
  "status": "success"
}

Endpoint: PATCH /api/ivanti/fp-submissions/:id/status

Updates the lifecycle status of a submission.

  • Auth: requireAuth(db), requireGroup('Admin', 'Standard_User')
  • Ownership: verified
  • Request body:
{
  "lifecycle_status": "rejected"
}
  • Validates status is one of: submitted, approved, rejected, rework, resubmitted
  • Validates transition is allowed (cannot transition FROM approved)
  • On success: updates lifecycle_status and updated_at, inserts history row with previous and new status, logs audit
  • Response: { success: true, previousStatus: "submitted", newStatus: "rejected" }

Pure Helper Functions (exported for testing)

The following pure functions are extracted for testability:

  • validateFpWorkflowForm(body) — already exists, reused for edit validation
  • isAllowedFileExtension(filename) — already exists, reused
  • buildSubjectFilterRequest(findingIds) — already exists, reused for map endpoint
  • validateLifecycleTransition(currentStatus, newStatus) — new, returns { valid: boolean, error?: string }
  • mergeFindings(existingJson, newIds) — new, merges finding ID arrays, deduplicates, returns JSON string
  • buildSubmissionHistoryEntry(changeType, details, userId, username) — new, constructs a history record object

Ivanti API Calls

Uses existing helpers from backend/helpers/ivantiApi.js:

  • Update workflow: ivantiPost() to POST /client/{clientId}/workflowBatch/falsePositive/update with JSON body
  • Map findings: ivantiFormPost() to POST /client/{clientId}/workflowBatch/falsePositive/{uuid}/map with subjectFilterRequest
  • Attach file: ivantiMultipartPost() to POST /client/{clientId}/workflowBatch/falsePositive/{uuid}/attach with file buffer

Frontend

New Component: FpEditModal

Defined inline in frontend/src/components/pages/ReportingPage.js, following the existing FpWorkflowModal pattern.

Props:

  • open (boolean) — controls visibility
  • onClose (function) — close handler
  • submission (object) — the FP_Submission record to edit (null when closed)
  • queueItems (array) — user's current queue items (for adding findings)
  • onSuccess (function) — callback after successful edit, triggers data refresh

State:

  • name, reason, description, expirationDate, scopeOverride — editable form fields, initialized from submission
  • files — array of new File objects for upload
  • additionalFindingIds — selected queue items to add as findings
  • saving — boolean, disables form during save
  • errors — validation error map
  • result — operation result (success/failure)
  • activeTab — current tab: 'details' | 'findings' | 'attachments' | 'history'

UI Layout:

  • Modal overlay with dark backdrop (matching FpWorkflowModal)
  • Header: "Edit FP Workflow — {workflow_name}" with lifecycle status badge and close button
  • Tab bar: Details | Findings | Attachments | History
  • Details tab: editable form fields (name, reason, description, expiration, scope override) with Save button
  • Findings tab: current finding IDs list (read-only) + mechanism to select and add FP queue items
  • Attachments tab: existing attachments list + file upload area for new attachments
  • History tab: chronological list of changes from ivanti_fp_submission_history
  • Footer: contextual action buttons per tab
  • Approved submissions: all fields read-only with "This submission is finalized" message

Workflow Badge Modifications (Reporting Table)

The workflow column renderer (lines 10441070 of ReportingPage.js) is modified:

  • For badges with state reworked, rejected, or expired:
    • Add cursor: 'pointer' and onClick handler
    • Append a small pencil icon (lucide Edit3, 10px) after the state text
    • On hover: increase border opacity and brighten background
    • On click: look up matching FP_Submission by wf.id (workflow batch ID), open FpEditModal
  • For badges with state requested or approved:
    • No changes — remain non-interactive (no cursor, no icon, no click handler)

QueuePanel Modifications

  • Add a "Submissions" section below the existing queue items list
  • Fetches submissions via GET /api/ivanti/fp-submissions on panel open
  • Each submission row shows: workflow name, batch ID, lifecycle status badge, finding count, created date
  • Lifecycle status badges use color coding: submitted (sky blue), approved (emerald), rejected (red), rework (amber), resubmitted (sky blue)
  • Clicking a submission row opens FpEditModal with that submission's data
  • Viewers see the list but cannot click to edit

Lifecycle Status Badge Component

Inline helper function lifecycleStatusBadge(status) returning style object:

Status Border Background Text
submitted rgba(14,165,233,0.4) rgba(14,165,233,0.12) #0EA5E9
approved rgba(16,185,129,0.4) rgba(16,185,129,0.12) #10B981
rejected rgba(239,68,68,0.4) rgba(239,68,68,0.12) #EF4444
rework rgba(245,158,11,0.4) rgba(245,158,11,0.12) #F59E0B
resubmitted rgba(14,165,233,0.4) rgba(14,165,233,0.12) #0EA5E9

Data Models

Schema Changes to ivanti_fp_submissions

Three new columns added to the existing table:

ALTER TABLE ivanti_fp_submissions ADD COLUMN lifecycle_status TEXT NOT NULL DEFAULT 'submitted'
    CHECK(lifecycle_status IN ('submitted', 'approved', 'rejected', 'rework', 'resubmitted'));

ALTER TABLE ivanti_fp_submissions ADD COLUMN ivanti_workflow_batch_uuid TEXT;

ALTER TABLE ivanti_fp_submissions ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP;

New Table: ivanti_fp_submission_history

CREATE TABLE IF NOT EXISTS ivanti_fp_submission_history (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    submission_id INTEGER NOT NULL,
    user_id INTEGER NOT NULL,
    username TEXT NOT NULL,
    change_type TEXT NOT NULL CHECK(change_type IN (
        'created', 'fields_updated', 'findings_added',
        'attachments_added', 'status_changed'
    )),
    change_details_json TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (submission_id) REFERENCES ivanti_fp_submissions(id) ON DELETE CASCADE
);

CREATE INDEX IF NOT EXISTS idx_fp_history_submission ON ivanti_fp_submission_history(submission_id);

change_details_json examples:

  • fields_updated: {"changed": {"name": {"from": "old", "to": "new"}, "reason": {"from": "old", "to": "new"}}}
  • findings_added: {"addedFindingIds": ["123", "456"], "queueItemIds": [3, 4]}
  • attachments_added: {"files": [{"filename": "evidence.pdf", "success": true}]}
  • status_changed: {"from": "submitted", "to": "rejected"}
  • created: {"workflowBatchId": 33418832, "findingCount": 3, "attachmentCount": 1}

Migration Script: backend/migrations/add_fp_submission_editing.js

Applies all schema changes idempotently using ALTER TABLE ... ADD COLUMN wrapped in try/catch (SQLite throws if column already exists) and CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS.

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.

Note: Properties for validateFpWorkflowForm and isAllowedFileExtension are already covered by the existing ivanti-fp-workflow-submission spec and are reused without modification. The properties below cover new pure functions introduced by this feature.

Property 1: Finding Merge Preserves All IDs and Deduplicates

For any existing finding IDs JSON string (valid JSON array of strings) and any array of new finding ID strings, mergeFindings(existingJson, newIds) should produce a JSON string that, when parsed, contains every ID from the original array and every ID from the new array, contains no duplicate entries, and has a length less than or equal to the sum of the original and new array lengths.

Validates: Requirements 3.3

Property 2: Lifecycle Transition Validation

For any pair of lifecycle status values (currentStatus, newStatus) drawn from the set {submitted, approved, rejected, rework, resubmitted}, validateLifecycleTransition(currentStatus, newStatus) should return { valid: false } whenever currentStatus is "approved" (no transitions allowed from finalized state), and should return { valid: true } for all other currentStatus values when newStatus is a valid lifecycle status. Additionally, when currentStatus is "rejected" or "rework" and newStatus is "resubmitted", the transition should always be valid.

Validates: Requirements 5.4, 5.5

Error Handling

Ivanti API Errors

HTTP Status Endpoint User-Facing Message System Behavior
401 All "Ivanti API key is invalid or missing. Contact your administrator." Log error, preserve form state
419 All "API key lacks permissions for this operation." Log error, preserve form state
429 All "Ivanti API rate limit reached. Please try again in a few minutes." Log error, preserve form state
5xx All "Ivanti API is temporarily unavailable. Please try again later." Log error, preserve form state
Other All "Operation failed: {status} — {message}" Log error with full response, preserve form state

Partial Failure (Attachment Upload)

When some attachment uploads succeed and others fail:

  • Response includes per-file success/failure details
  • Successfully uploaded files are recorded in attachment_results_json
  • Failed files are reported to the user with retry option
  • The submission record is updated with the successful uploads only

Lifecycle Guard Errors

  • Attempting to edit an "approved" submission returns 400: "This submission is finalized and cannot be edited."
  • Attempting an invalid status transition returns 400: "Cannot transition from {current} to {new}."

Ownership Errors

  • All edit endpoints verify user_id matches the authenticated user
  • Mismatch returns 403: "You can only edit your own submissions."

Local Database Errors

  • If history INSERT fails: log error, still return success (the Ivanti operation succeeded)
  • If audit log INSERT fails: fire-and-forget (existing logAudit() pattern)
  • If submission record UPDATE fails: return 500 with error message

Testing Strategy

Property-Based Testing

Use fast-check as the property-based testing library. Each correctness property maps to a single property-based test with a minimum of 100 iterations.

Property tests focus on the new pure functions:

  • mergeFindings(existingJson, newIds) — Property 1
  • validateLifecycleTransition(currentStatus, newStatus) — Property 2

Tag format: Feature: fp-submission-editing, Property {number}: {title}

Test file: backend/__tests__/fpSubmissionEditing.property.test.js

Unit Testing

Unit tests cover specific examples, edge cases, and integration points:

  • Validation reuse: verify validateFpWorkflowForm is called correctly in the PUT endpoint
  • Lifecycle badge styles: verify each of the 5 statuses maps to the correct color scheme
  • Clickable badge logic: verify reworked/rejected/expired states produce clickable badges, requested/approved do not
  • Ownership verification: verify 403 when non-owner attempts edit
  • Role guard: verify non-Admin/Standard_User users are rejected
  • Approved guard: verify 400 when editing an approved submission
  • Error mapping: verify each Ivanti HTTP status maps to the correct error message
  • History recording: verify correct change_type and change_details_json for each operation type
  • Migration idempotency: verify migration can run multiple times without error

Test file: backend/__tests__/fpSubmissionEditing.test.js

Integration Testing

Integration tests verify the full request/response cycle with mocked Ivanti API:

  • GET submissions returns correct records for authenticated user
  • PUT update proxies to Ivanti and updates local record
  • POST findings maps to Ivanti and merges finding IDs
  • POST attachments uploads to Ivanti and updates attachment records
  • PATCH status updates lifecycle and creates history entry
  • Queue items marked complete after successful finding addition

Test file: backend/__tests__/fpSubmissionEditing.integration.test.js