- Add GET /api/ivanti/fp-workflow/documents/search endpoint for querying the document library - Update POST /api/ivanti/fp-workflow to accept libraryDocIds for attaching library documents on create - Update POST .../submissions/:id/attachments to accept libraryDocIds on edit - Add AttachmentSourcePicker component with local upload and library search modes - Integrate picker into FpWorkflowModal (create) and FpEditModal (edit) - Track attachment source (local/library) in attachment_results_json for traceability
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
- User opens FP Create or Edit modal
- Attachment Source Picker renders with two mode tabs: Local Upload and Library
- In Library mode, user types a search query → frontend debounces 300ms → calls
GET /api/documents/search?q=... - User selects library documents and/or local files
- On submit:
- Frontend sends
FormDatawith local files inattachmentsfield and library document IDs in alibraryDocIdsJSON field - Backend parses both, looks up library documents in the
documentstable, reads their files from disk - Backend combines local file buffers and library file buffers into a single
filesarray - Backend calls
ivantiFormPostwith all files in one multipart request - Backend records results in
attachment_results_jsonwith asourcefield ("local"or"library")
- Frontend sends
Key Design Decisions
-
Single shared component: The
AttachmentSourcePickeris used in both modals to avoid duplication. It receives callbacks for state management and renders the mode toggle, search UI, and unified attachment list. -
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 existingattachmentsfile field. This keeps the existing local upload path unchanged. -
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. -
No new database tables: The feature uses the existing
documentstable for search and the existingivanti_fp_submissionstable for recording results. The only schema-level change is adding asourcefield to the JSON objects inattachment_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 thedocumentstable
Backend changes:
- Parse
libraryDocIdsfromreq.body(default to[]) - Validate each ID is a positive integer
- Query
documentstable for matching records - Validate all IDs were found (400 if any missing)
- Read each file from disk using
file_path(error if file missing on disk) - Combine local file buffers (
req.files) and library file buffers into a singleformFilesarray - Pass combined array to
ivantiFormPost - Record results with
source: "local"orsource: "library"inattachment_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 valuesearchResults— array of document records from APIsearching— 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
idto 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
libraryDocsstate array alongside existingfilesstate - On submit, append
libraryDocIdsas JSON string toFormData: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
FormDatawith both local files andlibraryDocIds - 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_jsonentries without asourcefield are treated as"local"by the frontend - The
libraryDocIdsfield is optional in both create and edit endpoints — omitting it preserves current behavior exactly - No database migrations required — the
documentstable 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