Files
cve-dashboard/.kiro/specs/fp-attachment-library/design.md

16 KiB

Design Document: FP Attachment Library

Overview

This feature extends the FP submission workflow to let users attach documents from the existing CVE document library (the documents table) alongside traditional local file uploads. The core change is a new Attachment Source Picker component shared by both the create and edit modals, backed by a new Document Search API endpoint. On submission, the backend reads library files from disk and sends them to the Ivanti API identically to local uploads.

The design prioritizes minimal disruption to the existing codebase: one new GET endpoint, modifications to two existing POST endpoints, and a shared React component inserted into both modals.

flowchart LR
    subgraph Frontend
        A[FpWorkflowModal] --> C[AttachmentSourcePicker]
        B[FpEditModal] --> C
        C -->|local files| D[File objects in state]
        C -->|library docs| E[Document IDs in state]
    end
    subgraph Backend
        F[GET /api/documents/search] -->|SQLite| G[(documents table)]
        H[POST /api/ivanti/fp-workflow] -->|reads disk| I[uploads/]
        H -->|multipart| J[Ivanti API]
        K[POST .../attachments] -->|reads disk| I
        K -->|multipart| J
    end
    C -->|fetch| F
    A -->|FormData + libraryDocIds| H
    B -->|FormData + libraryDocIds| K

Architecture

Request Flow

  1. User opens FP Create or Edit modal
  2. Attachment Source Picker renders with two mode tabs: Local Upload and Library
  3. In Library mode, user types a search query → frontend debounces 300ms → calls GET /api/documents/search?q=...
  4. User selects library documents and/or local files
  5. On submit:
    • Frontend sends FormData with local files in attachments field and library document IDs in a libraryDocIds JSON field
    • Backend parses both, looks up library documents in the documents table, reads their files from disk
    • Backend combines local file buffers and library file buffers into a single files array
    • Backend calls ivantiFormPost with all files in one multipart request
    • Backend records results in attachment_results_json with a source field ("local" or "library")

Key Design Decisions

  1. Single shared component: The AttachmentSourcePicker is used in both modals to avoid duplication. It receives callbacks for state management and renders the mode toggle, search UI, and unified attachment list.

  2. Library doc IDs sent as JSON field: Rather than changing the multipart structure, library document IDs are sent as a JSON-encoded string field (libraryDocIds) alongside the existing attachments file field. This keeps the existing local upload path unchanged.

  3. Backend reads files from disk: Library documents are read from disk using fs.readFileSync(file_path) at submission time. This avoids storing duplicate file buffers and keeps the Ivanti API call identical for both sources.

  4. No new database tables: The feature uses the existing documents table for search and the existing ivanti_fp_submissions table for recording results. The only schema-level change is adding a source field to the JSON objects in attachment_results_json.

Components and Interfaces

New API Endpoint

GET /api/documents/search

Added to backend/routes/ivantiFpWorkflow.js (or as a new route in server.js alongside existing document routes).

Parameter Type Required Description
q query string No Search term matched against name, cve_id, vendor using SQL LIKE

Response (200):

[
  {
    "id": 42,
    "cve_id": "CVE-2024-1234",
    "vendor": "Microsoft",
    "name": "advisory-2024-1234.pdf",
    "type": "Advisory",
    "file_size": "245760",
    "mime_type": "application/pdf",
    "uploaded_at": "2024-11-15T10:30:00.000Z"
  }
]

Implementation:

// Pseudocode
router.get('/documents/search', requireAuth(db), (req, res) => {
    const q = (req.query.q || '').trim();
    let sql, params;
    if (q) {
        const like = `%${q}%`;
        sql = `SELECT id, cve_id, vendor, name, type, file_size, mime_type, uploaded_at
               FROM documents
               WHERE name LIKE ? OR cve_id LIKE ? OR vendor LIKE ?
               ORDER BY uploaded_at DESC
               LIMIT 50`;
        params = [like, like, like];
    } else {
        sql = `SELECT id, cve_id, vendor, name, type, file_size, mime_type, uploaded_at
               FROM documents
               ORDER BY uploaded_at DESC
               LIMIT 50`;
        params = [];
    }
    db.all(sql, params, (err, rows) => {
        if (err) return res.status(500).json({ error: 'Database error.' });
        res.json(rows || []);
    });
});

Modified API Endpoints

POST /api/ivanti/fp-workflow (create)

New field in multipart body:

  • libraryDocIds — JSON-encoded array of document IDs (integers) from the documents table

Backend changes:

  1. Parse libraryDocIds from req.body (default to [])
  2. Validate each ID is a positive integer
  3. Query documents table for matching records
  4. Validate all IDs were found (400 if any missing)
  5. Read each file from disk using file_path (error if file missing on disk)
  6. Combine local file buffers (req.files) and library file buffers into a single formFiles array
  7. Pass combined array to ivantiFormPost
  8. Record results with source: "local" or source: "library" in attachment_results_json

POST /api/ivanti/fp-workflow/submissions/:id/attachments (edit)

Same changes as the create endpoint — accepts libraryDocIds alongside attachments files.

New Frontend Component

AttachmentSourcePicker

Defined inline in ReportingPage.js (consistent with existing component patterns).

Props:

Prop Type Description
files File[] Current local file attachments
onFilesChange (files: File[]) => void Callback when local files change
libraryDocs object[] Current selected library documents
onLibraryDocsChange (docs: object[]) => void Callback when library selections change
disabled boolean Disables all interactions (for approved submissions)

Internal state:

  • mode'local' or 'library' (default: 'local')
  • searchQuery — current search input value
  • searchResults — array of document records from API
  • searching — loading state for search

Behavior:

  • Mode toggle renders two tab-style buttons at the top
  • Local mode shows the existing drag-and-drop zone
  • Library mode shows a search input + scrollable results list
  • Search is debounced at 300ms using setTimeout/clearTimeout
  • Selected library docs are tracked by id to prevent duplicates
  • Already-selected docs appear disabled/checked in search results
  • Unified attachment list below shows all attachments with source badges
  • Each attachment row shows: source badge, filename, file size, remove button
  • Library attachment rows additionally show CVE ID and vendor
stateDiagram-v2
    [*] --> LocalMode
    LocalMode --> LibraryMode: Click "Library" tab
    LibraryMode --> LocalMode: Click "Local Upload" tab
    
    state LibraryMode {
        [*] --> Idle
        Idle --> Searching: User types (after 300ms debounce)
        Searching --> ResultsShown: API responds
        ResultsShown --> Searching: User types again
        ResultsShown --> DocSelected: User clicks result
        DocSelected --> ResultsShown: Doc added to list
    }

Integration Points

FpWorkflowModal (create)

  • Replace the current file upload section with <AttachmentSourcePicker>
  • Add libraryDocs state array alongside existing files state
  • On submit, append libraryDocIds as JSON string to FormData:
    formData.append('libraryDocIds', JSON.stringify(libraryDocs.map(d => d.id)));
    

FpEditModal (edit — attachments tab)

  • Replace the static "upload in Ivanti" message with <AttachmentSourcePicker>
  • Keep existing attachment display above the picker
  • On submit, build FormData with both local files and libraryDocIds
  • Disable picker when lifecycle_status === 'approved'

Data Models

Existing: documents table (no changes)

Column Type Description
id INTEGER PK Auto-increment ID
cve_id VARCHAR(20) Associated CVE identifier
vendor VARCHAR(100) Vendor name
name VARCHAR(255) Original filename
type VARCHAR(50) Document type (Advisory, Patch, etc.)
file_path VARCHAR(500) Relative path under uploads/
file_size VARCHAR(20) Human-readable or byte size
mime_type VARCHAR(100) MIME type
uploaded_at TIMESTAMP Upload timestamp
notes TEXT Optional notes

Modified: attachment_results_json shape

Current format per entry:

{ "filename": "report.pdf", "success": true }

New format per entry:

{ "filename": "report.pdf", "success": true, "source": "local" }

or:

{ "filename": "advisory-2024.pdf", "success": true, "source": "library", "documentId": 42 }

The source field is added to distinguish attachment origins. The documentId field is included for library documents to enable traceability. Existing records without a source field are treated as "local" by the frontend for backward compatibility.

Frontend State: Library Document Selection

// Shape of a selected library document in component state
{
  id: 42,                          // documents.id
  cve_id: "CVE-2024-1234",
  vendor: "Microsoft",
  name: "advisory-2024-1234.pdf",
  file_size: "245760",
  mime_type: "application/pdf"
}

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: Search results match query

For any non-empty search query string q and any set of documents in the database, every document returned by the Document Search API SHALL have q as a case-insensitive substring of its name, cve_id, or vendor field.

Validates: Requirements 1.1

Property 2: Default results are ordered by recency

For any set of documents in the database, when the Document Search API is called with no query, the returned results SHALL be ordered by uploaded_at descending (most recent first).

Validates: Requirements 1.2

Property 3: Result set size is bounded

For any search query (including empty), the Document Search API SHALL return at most 50 records.

Validates: Requirements 1.3

Property 4: Library document ID validation rejects non-positive-integers

For any value that is not a positive integer (e.g., negative numbers, zero, floats, non-numeric strings, null), the backend validation SHALL reject it as an invalid library document ID.

Validates: Requirements 4.5

Property 5: Combined attachments are all sent to Ivanti

For any combination of local file uploads and library document references in a submission, the backend SHALL produce a files array for the Ivanti API call whose length equals the count of local files plus the count of valid library documents, and each library file buffer SHALL match the content read from the document's file_path.

Validates: Requirements 4.1, 4.2

Property 6: Attachment results record source and filename correctly

For any mix of local and library attachments processed by the backend, each entry in attachment_results_json SHALL have a source field of "local" or "library", and for library entries the filename SHALL equal the name field from the corresponding documents record.

Validates: Requirements 4.6, 4.7

Property 7: No duplicate library documents in attachment list

For any sequence of library document selections applied to the Attachment Source Picker, the resulting attachment list SHALL contain at most one entry per document id.

Validates: Requirements 5.1

Property 8: Attachment list displays all required fields per type

For any attachment in the list (local or library), the rendered display SHALL include the filename, file size, source indicator, and a remove action. For any library attachment, the display SHALL additionally include the CVE ID and vendor name.

Validates: Requirements 6.1, 6.2, 6.3, 6.4

Error Handling

Scenario Behavior
Document search DB error Return 500 with { error: 'Database error.' }
Invalid libraryDocIds JSON Return 400 with { error: 'libraryDocIds must be a valid JSON array.' }
Non-positive-integer document ID Return 400 identifying the invalid ID
Document ID not found in DB Return 400 identifying the missing document ID
Library file missing from disk Log warning, skip that attachment, include { success: false, error: 'File not found on disk' } in attachment results, continue with remaining files
Ivanti API failure for attachment upload Record { success: false, error: '...' } per file in results, return partial success if some files succeeded
Network error calling Document Search API (frontend) Show inline error message in search results area, allow retry
Empty search results Show "No documents found" message with suggestion to refine search
Unauthenticated request to search endpoint Return 401 (handled by existing requireAuth middleware)

Backward Compatibility

  • Existing attachment_results_json entries without a source field are treated as "local" by the frontend
  • The libraryDocIds field is optional in both create and edit endpoints — omitting it preserves current behavior exactly
  • No database migrations required — the documents table already exists

Testing Strategy

Property-Based Tests (fast-check)

The project uses plain JavaScript with React 19. Property-based tests will use fast-check with the existing react-scripts test runner (Jest).

Each property test runs a minimum of 100 iterations and is tagged with a comment referencing its design property.

Configuration: npm install --save-dev fast-check in the frontend package (or backend if testing backend logic separately).

Properties to test:

  • Property 1: Search relevance — generate random documents and queries, verify all results match
  • Property 2: Default ordering — generate random documents, verify descending order
  • Property 3: Result limit — generate >50 documents, verify max 50 returned
  • Property 4: ID validation — generate random non-positive-integer values, verify rejection
  • Property 5: Combined attachment handling — generate random mixes, verify file array correctness
  • Property 6: Result record shape — generate random mixes, verify source and filename fields
  • Property 7: Duplicate prevention — generate random selection sequences, verify uniqueness
  • Property 8: Display completeness — generate random attachment lists, verify rendered fields

Tag format: // Feature: fp-attachment-library, Property N: <property text>

Unit Tests (example-based)

  • Authentication guard on search endpoint (1.5)
  • DB error handling returns 500 (1.6)
  • Mode toggle renders correctly in both modals (2.1, 2.2, 2.3)
  • Debounce behavior with fake timers (2.4)
  • Library doc selection adds to list with indicator (2.5)
  • Remove works for both types (2.6)
  • Mixed attachments in same submission (2.7)
  • Library doc displays name, size, CVE ID (2.8)
  • Edit modal replaces static message (3.1)
  • Existing attachments shown above picker (3.4)
  • Approved submission disables picker (3.5)
  • Missing file on disk returns error (4.3)
  • Invalid document ID returns 400 (4.4)
  • Already-selected docs shown as disabled (5.2)
  • Removed doc re-enabled in results (5.3)

Integration Tests

  • End-to-end create flow with mixed local + library attachments
  • End-to-end edit flow adding library attachments to existing submission
  • Search endpoint with real SQLite database