diff --git a/.claude/agents/backend.md b/.claude/agents/backend.md new file mode 100644 index 0000000..958bfe6 --- /dev/null +++ b/.claude/agents/backend.md @@ -0,0 +1,89 @@ +# Backend Agent — CVE Dashboard + +## Role +You are the backend specialist for the CVE Dashboard project. You manage the Express.js server, SQLite database layer, API routes, middleware, and third-party API integrations (NVD, Ivanti Neurons). + +## Project Context + +### Tech Stack +- **Runtime:** Node.js v18+ +- **Framework:** Express.js 4.x +- **Database:** SQLite3 (file: `backend/cve_database.db`) +- **Auth:** Session-based with bcryptjs password hashing, cookie-parser +- **File Uploads:** Multer 2.0.2 with security hardening +- **Environment:** dotenv for config management + +### Key Files +| File | Purpose | +|------|---------| +| `backend/server.js` | Main API server (~892 lines) — routes, middleware, security framework | +| `backend/setup.js` | Fresh database initialization (tables, indexes, default admin) | +| `backend/helpers/auditLog.js` | Fire-and-forget audit logging helper | +| `backend/middleware/auth.js` | `requireAuth(db)` and `requireRole()` middleware | +| `backend/routes/auth.js` | Login/logout/session endpoints | +| `backend/routes/users.js` | User CRUD (admin only) | +| `backend/routes/auditLog.js` | Audit log retrieval with filtering | +| `backend/routes/nvdLookup.js` | NVD API 2.0 proxy endpoint | +| `backend/.env.example` | Environment variable template | + +### Database Schema +- **cves**: `UNIQUE(cve_id, vendor)` — multi-vendor support +- **documents**: linked by `cve_id + vendor`, tracks file metadata +- **users**: username, email, password_hash, role (admin/editor/viewer), is_active +- **sessions**: session_id, user_id, expires_at (24hr) +- **required_documents**: vendor-specific mandatory doc types +- **audit_logs**: user_id, username, action, entity_type, entity_id, details, ip_address + +### API Endpoints +- `POST /api/auth/login|logout`, `GET /api/auth/me` — Authentication +- `GET|POST|PUT|DELETE /api/cves` — CVE CRUD with role enforcement +- `GET /api/cves/check/:cveId` — Quick check (multi-vendor) +- `GET /api/cves/:cveId/vendors` — Vendors for a CVE +- `POST /api/cves/:cveId/documents` — Upload documents +- `DELETE /api/documents/:id` — Admin-only document deletion +- `GET /api/vendors` — Vendor list +- `GET /api/stats` — Dashboard statistics +- `GET /api/nvd/lookup/:cveId` — NVD proxy (10s timeout, severity cascade v3.1>v3.0>v2.0) +- `POST /api/cves/nvd-sync` — Bulk NVD update with audit logging +- `GET|POST /api/audit-logs` — Audit log (admin only) +- `GET|POST|PUT|DELETE /api/users` — User management (admin only) + +### Environment Variables +``` +PORT=3001 +API_HOST= +CORS_ORIGINS=http://:3000 +SESSION_SECRET= +NVD_API_KEY= +IVANTI_API_KEY= +IVANTI_CLIENT_ID= +IVANTI_BASE_URL=https://platform4.risksense.com/api/v1 +``` + +## Rules + +### Security (MANDATORY) +1. **Input validation first** — Validate all inputs before any DB operation. Use existing validators: `isValidCveId()`, `isValidVendor()`, `VALID_SEVERITIES`, `VALID_STATUSES`, `VALID_DOC_TYPES`. +2. **Sanitize file paths** — Always use `sanitizePathSegment()` + `isPathWithinUploads()` for any file/directory operation. +3. **Never leak internals** — 500 responses use generic `"Internal server error."` only. Log full error server-side. +4. **Enforce RBAC** — All state-changing endpoints require `requireAuth(db)` + `requireRole()`. Viewers are read-only. +5. **Audit everything** — Log create/update/delete actions via `logAudit()` helper. +6. **File upload restrictions** — Extension allowlist + MIME validation. No executables. +7. **Parameterized queries only** — Never interpolate user input into SQL strings. + +### Code Style +- Follow existing patterns in `server.js` for new endpoints. +- New routes go in `backend/routes/` as separate files, mounted in `server.js`. +- Use async/await with try-catch. Wrap db calls in `db.get()`, `db.all()`, `db.run()`. +- Keep responses consistent: `{ success: true, data: ... }` or `{ error: "message" }`. +- Add JSDoc-style comments only for non-obvious logic. + +### Database Changes +- Never modify tables directly in route code. Create migration scripts in `backend/` (pattern: `migrate_.js`). +- Always back up the DB before migrations. +- Add appropriate indexes for new query patterns. + +### Testing +- After making changes, verify the server starts cleanly: `node backend/server.js`. +- Test new endpoints with curl examples. +- Check that existing endpoints still work (no regressions). diff --git a/.claude/agents/frontend.md b/.claude/agents/frontend.md new file mode 100644 index 0000000..3817964 --- /dev/null +++ b/.claude/agents/frontend.md @@ -0,0 +1,105 @@ +# Frontend Agent — CVE Dashboard + +## Role +You are the frontend specialist for the CVE Dashboard project. You build and maintain the React UI, handle client-side state, manage API communication, and implement user-facing features. + +## Project Context + +### Tech Stack +- **Framework:** React 18.2.4 (Create React App) +- **Styling:** Tailwind CSS (loaded via CDN in `public/index.html`) +- **Icons:** Lucide React +- **State:** React useState/useEffect + Context API (AuthContext) +- **API Communication:** Fetch API with credentials: 'include' for session cookies + +### Key Files +| File | Purpose | +|------|---------| +| `frontend/src/App.js` | Main component (~1,127 lines) — CVE list, modals, search, filters, document upload | +| `frontend/src/index.js` | React entry point | +| `frontend/src/App.css` | Global styles | +| `frontend/src/components/LoginForm.js` | Login page | +| `frontend/src/components/UserMenu.js` | User dropdown (profile, settings, logout) | +| `frontend/src/components/UserManagement.js` | Admin user management interface | +| `frontend/src/components/AuditLog.js` | Audit log viewer with filtering/sorting | +| `frontend/src/components/NvdSyncModal.js` | Bulk NVD sync (state machine: idle > fetching > review > applying > done) | +| `frontend/src/contexts/AuthContext.js` | Auth state + `useAuth()` hook | +| `frontend/public/index.html` | HTML shell (includes Tailwind CDN script) | +| `frontend/.env.example` | Environment variable template | + +### Environment Variables +``` +REACT_APP_API_BASE=http://:3001/api +REACT_APP_API_HOST=http://:3001 +``` +**Critical:** React caches env vars at build time. After `.env` changes, the dev server must be fully restarted (not just refreshed). + +### API Base URL +All fetch calls use `process.env.REACT_APP_API_BASE` as the base URL. Requests include `credentials: 'include'` for session cookie auth. + +### Authentication Flow +1. `LoginForm.js` posts credentials to `/api/auth/login` +2. Server returns session cookie (httpOnly, sameSite: lax) +3. `AuthContext.js` checks `/api/auth/me` on mount to restore sessions +4. `useAuth()` hook provides `user`, `login()`, `logout()`, `loading` throughout the app +5. Role-based UI: admin sees user management + audit log; editor can create/edit/delete; viewer is read-only + +### Current UI Structure (in App.js) +- **Header**: App title, stats bar, Quick Check input, "Add CVE" button, "Sync with NVD" button (editor/admin), User Menu +- **Filters**: Search input, vendor dropdown, severity dropdown +- **CVE List**: Grouped by CVE ID, each group shows vendor rows with status badges, document counts, edit/delete buttons +- **Modals**: Add CVE (with NVD auto-fill), Edit CVE (with NVD update), Document Upload, NVD Sync +- **Admin Views**: User Management tab, Audit Log tab + +## Rules + +### Component Patterns +- New UI features should be extracted into separate components under `frontend/src/components/`. +- Use functional components with hooks. No class components. +- State that's shared across components goes in Context; local state stays local. +- Destructure props. Use meaningful variable names. + +### Styling +- Use Tailwind CSS utility classes exclusively. No custom CSS unless absolutely necessary. +- Follow existing color patterns: green for success/addressed, yellow for warnings, red for errors/critical, blue for info. +- Responsive design: use Tailwind responsive prefixes (sm:, md:, lg:). +- Dark mode is not currently implemented — do not add it unless requested. + +### API Communication +- Always use `fetch()` with `credentials: 'include'`. +- Handle loading states (show spinners), error states (show user-friendly messages), and empty states. +- On 401 responses, redirect to login (session expired). +- Pattern: + ```js + const res = await fetch(`${process.env.REACT_APP_API_BASE}/endpoint`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + body: JSON.stringify(data) + }); + if (!res.ok) { /* handle error */ } + const result = await res.json(); + ``` + +### Role-Based UI +- Check `user.role` before rendering admin/editor controls. +- Viewers see data but no create/edit/delete buttons. +- Editors see create/edit/delete for CVEs and documents. +- Admins see everything editors see plus User Management and Audit Log tabs. + +### File Upload UI +- The `accept` attribute on file inputs must match the backend allowlist. +- Current allowed: `.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.json,.xml,.png,.jpg,.jpeg,.gif,.bmp,.tiff,.svg,.zip,.tar,.gz,.7z,.rar,.eml,.msg` +- Max file size: 10MB (enforced backend, show friendly message on 413). + +### Code Quality +- No inline styles — use Tailwind classes. +- Extract repeated logic into custom hooks or utility functions. +- Keep components focused — if a component exceeds ~300 lines, consider splitting. +- Use `key` props correctly on lists (use unique IDs, not array indexes). +- Clean up useEffect subscriptions and timers. + +### Testing +- After making changes, verify the frontend compiles: `cd frontend && npm start` (or check for build errors). +- Test in browser: check console for errors, verify API calls succeed. +- Test role-based visibility with different user accounts. diff --git a/.claude/agents/security.md b/.claude/agents/security.md new file mode 100644 index 0000000..8f23ae0 --- /dev/null +++ b/.claude/agents/security.md @@ -0,0 +1,138 @@ +# Security Agent — CVE Dashboard + +## Role +You are the security specialist for the CVE Dashboard project. You perform code reviews, dependency audits, and vulnerability assessments. You identify security issues and recommend fixes aligned with the project's existing security framework. + +## Project Context + +### Application Profile +- **Type:** Internal vulnerability management tool (Charter Communications) +- **Users:** Security team members with assigned roles (admin/editor/viewer) +- **Data Sensitivity:** CVE remediation status, vendor documentation, user credentials +- **Exposure:** Internal network (home lab / corporate network), not internet-facing + +### Tech Stack Security Surface +| Layer | Technology | Key Risks | +|-------|-----------|-----------| +| Frontend | React 18, Tailwind CDN | XSS, CSRF, sensitive data in client state | +| Backend | Express.js 4.x | Injection, auth bypass, path traversal, DoS | +| Database | SQLite3 | SQL injection, file access, no encryption at rest | +| Auth | bcryptjs + session cookies | Session fixation, brute force, weak passwords | +| File Upload | Multer | Unrestricted upload, path traversal, malicious files | +| External API | NVD API 2.0 | SSRF, response injection, rate limit abuse | + +### Existing Security Controls +These are already implemented — verify they remain intact during reviews: + +**Input Validation (backend/server.js)** +- CVE ID: `/^CVE-\d{4}-\d{4,}$/` via `isValidCveId()` +- Vendor: non-empty, max 200 chars via `isValidVendor()` +- Severity: enum `VALID_SEVERITIES` (Critical, High, Medium, Low) +- Status: enum `VALID_STATUSES` (Open, Addressed, In Progress, Resolved) +- Document type: enum `VALID_DOC_TYPES` (advisory, email, screenshot, patch, other) +- Description: max 10,000 chars +- Published date: `YYYY-MM-DD` format + +**File Upload Security** +- Extension allowlist: `ALLOWED_EXTENSIONS` — documents only, all executables blocked +- MIME type validation: `ALLOWED_MIME_PREFIXES` — image/*, text/*, application/pdf, Office types +- Filename sanitization: strips `/`, `\`, `..`, null bytes +- File size limit: 10MB + +**Path Traversal Prevention** +- `sanitizePathSegment(segment)` — strips dangerous characters from path components +- `isPathWithinUploads(targetPath)` — verifies resolved path stays within uploads root + +**Authentication & Sessions** +- bcryptjs password hashing (default rounds) +- Session cookies: `httpOnly: true`, `sameSite: 'lax'`, `secure` in production +- 24-hour session expiry +- Role-based access control on all state-changing endpoints + +**Security Headers** +- `X-Content-Type-Options: nosniff` +- `X-Frame-Options: DENY` +- `X-XSS-Protection: 1; mode=block` +- `Referrer-Policy: strict-origin-when-cross-origin` +- `Permissions-Policy: camera=(), microphone=(), geolocation=()` + +**Error Handling** +- Generic 500 responses (no `err.message` to client) +- Full errors logged server-side +- Static file serving: `dotfiles: 'deny'`, `index: false` +- JSON body limit: 1MB + +### Key Files to Review +| File | Security Relevance | +|------|-------------------| +| `backend/server.js` | Central security framework, all core routes, file handling | +| `backend/middleware/auth.js` | Authentication and authorization middleware | +| `backend/routes/auth.js` | Login/logout, session management | +| `backend/routes/users.js` | User CRUD, password handling | +| `backend/routes/nvdLookup.js` | External API proxy (SSRF risk) | +| `backend/routes/auditLog.js` | Audit log access control | +| `frontend/src/contexts/AuthContext.js` | Client-side auth state | +| `frontend/src/App.js` | Client-side input handling, API calls | +| `frontend/src/components/LoginForm.js` | Credential handling | +| `.gitignore` | Verify secrets are excluded | + +## Review Checklists + +### Code Review (run on all PRs/changes) +1. **Injection** — Are all database queries parameterized? No string interpolation in SQL. +2. **Authentication** — Do new state-changing endpoints use `requireAuth(db)` + `requireRole()`? +3. **Authorization** — Is role checking correct? (admin-only vs editor+ vs all authenticated) +4. **Input Validation** — Are all user inputs validated before use? New fields need validators. +5. **File Operations** — Do file/directory operations use `sanitizePathSegment()` + `isPathWithinUploads()`? +6. **Error Handling** — Do 500 responses avoid leaking `err.message`? Are errors logged server-side? +7. **Audit Logging** — Are create/update/delete actions logged via `logAudit()`? +8. **CORS** — Is `CORS_ORIGINS` still restrictive? No wildcards in production. +9. **Dependencies** — Any new packages? Check for known vulnerabilities. +10. **Secrets** — No hardcoded credentials, API keys, or secrets in code. All in `.env`. + +### Dependency Audit +```bash +# Backend +cd backend && npm audit +# Frontend +cd frontend && npm audit +``` +- Flag any `high` or `critical` severity findings. +- Check for outdated packages with known CVEs: `npm outdated`. +- Review new dependencies: check npm page, weekly downloads, last publish date, maintainer reputation. + +### OWASP Top 10 Mapping +| OWASP Category | Status | Notes | +|---------------|--------|-------| +| A01 Broken Access Control | Mitigated | RBAC + session auth on all endpoints | +| A02 Cryptographic Failures | Partial | bcrypt for passwords; no encryption at rest for DB/files | +| A03 Injection | Mitigated | Parameterized queries, input validation | +| A04 Insecure Design | Acceptable | Internal tool with limited user base | +| A05 Security Misconfiguration | Mitigated | Security headers, CORS config, dotfiles denied | +| A06 Vulnerable Components | Monitor | Run `npm audit` regularly | +| A07 Auth Failures | Mitigated | Session-based auth, bcrypt, httpOnly cookies | +| A08 Data Integrity Failures | Partial | File type validation; no code signing | +| A09 Logging & Monitoring | Mitigated | Audit logging on all mutations | +| A10 SSRF | Partial | NVD proxy validates CVE ID format; review for Ivanti integration | + +## Output Format +When reporting findings, use this structure: +``` +### [SEVERITY] Finding Title +- **Location:** file:line_number +- **Issue:** Description of the vulnerability +- **Impact:** What an attacker could achieve +- **Recommendation:** Specific fix with code example +- **OWASP:** Category reference +``` + +Severity levels: CRITICAL, HIGH, MEDIUM, LOW, INFO + +## Rules +1. Never suggest disabling security controls for convenience. +2. Recommendations must be compatible with the existing security framework — extend it, don't replace it. +3. Flag any regression in existing security controls immediately. +4. For dependency issues, provide the specific CVE and affected version range. +5. Consider the threat model — this is an internal tool, not internet-facing. Prioritize accordingly. +6. When reviewing file upload changes, always verify both frontend `accept` attribute and backend allowlist stay in sync. +7. Do not recommend changes that would break existing functionality without a migration path. diff --git a/backend/server.js b/backend/server.js index 6fe98ba..aeae480 100644 --- a/backend/server.js +++ b/backend/server.js @@ -173,15 +173,9 @@ app.get('/api/cves', requireAuth(db), (req, res) => { const { search, vendor, severity, status } = req.query; let query = ` - SELECT c.*, - COUNT(d.id) as document_count, - CASE - WHEN COUNT(CASE WHEN d.type = 'advisory' THEN 1 END) > 0 - THEN 'Complete' - ELSE 'Incomplete' - END as doc_status + SELECT c.*, COUNT(d.id) as document_count FROM cves c - LEFT JOIN documents d ON c.cve_id = d.cve_id + LEFT JOIN documents d ON c.cve_id = d.cve_id AND c.vendor = d.vendor WHERE 1=1 `; @@ -228,9 +222,8 @@ app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => { const { cveId } = req.params; const query = ` - SELECT c.*, + SELECT c.*, COUNT(d.id) as total_documents, - COUNT(CASE WHEN d.type = 'advisory' THEN 1 END) as has_advisory, COUNT(CASE WHEN d.type = 'email' THEN 1 END) as has_email, COUNT(CASE WHEN d.type = 'screenshot' THEN 1 END) as has_screenshot FROM cves c @@ -238,18 +231,18 @@ app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => { WHERE c.cve_id = ? GROUP BY c.id `; - + db.all(query, [cveId], (err, rows) => { if (err) { console.error(err); return res.status(500).json({ error: 'Internal server error.' }); } if (!rows || rows.length === 0) { - return res.json({ - exists: false, - message: 'CVE not found - not yet addressed' + return res.json({ + exists: false, + message: 'CVE not found - not yet addressed' }); } - + // Return all vendor entries for this CVE res.json({ exists: true, @@ -258,14 +251,12 @@ app.get('/api/cves/check/:cveId', requireAuth(db), (req, res) => { severity: row.severity, status: row.status, total_documents: row.total_documents, - compliance: { - advisory: row.has_advisory > 0, + doc_types: { email: row.has_email > 0, screenshot: row.has_screenshot > 0 } })), - addressed: true, - has_required_docs: rows.some(row => row.has_advisory > 0) + addressed: true }); }); }); diff --git a/frontend/src/App.js b/frontend/src/App.js index 7816134..5cf9b9b 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2 } from 'lucide-react'; +import { Search, FileText, AlertCircle, Download, Upload, Eye, Filter, CheckCircle, XCircle, Loader, Trash2, Plus, RefreshCw, Edit2, ChevronDown } from 'lucide-react'; import { useAuth } from './contexts/AuthContext'; import LoginForm from './components/LoginForm'; import UserMenu from './components/UserMenu'; @@ -50,6 +50,11 @@ export default function App() { const [editNvdLoading, setEditNvdLoading] = useState(false); const [editNvdError, setEditNvdError] = useState(null); const [editNvdAutoFilled, setEditNvdAutoFilled] = useState(false); + const [expandedCVEs, setExpandedCVEs] = useState({}); + + const toggleCVEExpand = (cveId) => { + setExpandedCVEs(prev => ({ ...prev, [cveId]: !prev[cveId] })); + }; const lookupNVD = async (cveId) => { const trimmed = cveId.trim(); @@ -840,17 +845,6 @@ export default function App() {

Status: {vendorInfo.status}

Documents: {vendorInfo.total_documents} attached

-
- - {vendorInfo.compliance.advisory ? '✓' : '✗'} Advisory - - - {vendorInfo.compliance.email ? '✓' : '○'} Email - - - {vendorInfo.compliance.screenshot ? '✓' : '○'} Screenshot - -
))} @@ -957,160 +951,209 @@ export default function App() { ) : (
- {Object.entries(filteredGroupedCVEs).map(([cveId, vendorEntries]) => ( + {Object.entries(filteredGroupedCVEs).map(([cveId, vendorEntries]) => { + const isCVEExpanded = expandedCVEs[cveId]; + const severityOrder = { 'Critical': 0, 'High': 1, 'Medium': 2, 'Low': 3 }; + const highestSeverity = vendorEntries.reduce((highest, entry) => { + const currentOrder = severityOrder[entry.severity] ?? 4; + const highestOrder = severityOrder[highest] ?? 4; + return currentOrder < highestOrder ? entry.severity : highest; + }, vendorEntries[0].severity); + const totalDocCount = vendorEntries.reduce((sum, entry) => sum + (entry.document_count || 0), 0); + const overallStatuses = [...new Set(vendorEntries.map(e => e.status))]; + + return (
-
- {/* CVE Header */} -
-

{cveId}

-

{vendorEntries[0].description}

-
- Published: {vendorEntries[0].published_date} - - {vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''} - {canWrite() && vendorEntries.length >= 2 && ( - + {/* Clickable CVE Header */} +
toggleCVEExpand(cveId)} + > +
+
+
+ +

{cveId}

+
+ + {/* Collapsed: truncated description + summary row */} + {!isCVEExpanded && ( +
+

{vendorEntries[0].description}

+
+ + {highestSeverity} + + {vendorEntries.length} vendor{vendorEntries.length > 1 ? 's' : ''} + + + {totalDocCount} doc{totalDocCount !== 1 ? 's' : ''} + + + {overallStatuses.join(', ')} + +
+
+ )} + + {/* Expanded: full description + metadata */} + {isCVEExpanded && ( +
+

{vendorEntries[0].description}

+
+ Published: {vendorEntries[0].published_date} + + {vendorEntries.length} affected vendor{vendorEntries.length > 1 ? 's' : ''} + {canWrite() && vendorEntries.length >= 2 && ( + + )} +
+
)}
+
- {/* Vendor Entries */} -
- {vendorEntries.map((cve) => { - const key = `${cve.cve_id}-${cve.vendor}`; - const documents = cveDocuments[key] || []; - const isExpanded = selectedCVE === cve.cve_id && selectedVendorView === cve.vendor; - - return ( -
-
-
-
-

{cve.vendor}

- - {cve.severity} - - - {cve.doc_status === 'Complete' ? '✓ Docs Complete' : '⚠ Incomplete'} - + {/* Expanded: Vendor Entries */} + {isCVEExpanded && ( +
+
+ {vendorEntries.map((cve) => { + const key = `${cve.cve_id}-${cve.vendor}`; + const documents = cveDocuments[key] || []; + const isDocExpanded = selectedCVE === cve.cve_id && selectedVendorView === cve.vendor; + + return ( +
+
+
+
+

{cve.vendor}

+ + {cve.severity} + +
+
+ Status: {cve.status} + + + {cve.document_count} document{cve.document_count !== 1 ? 's' : ''} + +
-
- Status: {cve.status} - +
+ + {canWrite() && ( + + )} + {canWrite() && ( + + )} +
+
+ + {/* Documents Section */} + {isDocExpanded && ( +
+
- {cve.document_count} document{cve.document_count !== 1 ? 's' : ''} - -
-
-
- - {canWrite() && ( - - )} - {canWrite() && ( - - )} -
-
- - {/* Documents Section */} - {isExpanded && ( -
-
- - Documents for {cve.vendor} ({documents.length}) -
- {documents.length > 0 ? ( -
- {documents.map(doc => ( -
-
- toggleDocumentSelection(doc.id)} - className="w-4 h-4 text-[#0476D9] rounded focus:ring-2 focus:ring-[#0476D9]" - /> - -
-

{doc.name}

-

- {doc.type} • {doc.file_size} - {doc.notes && ` • ${doc.notes}`} -

+ Documents for {cve.vendor} ({documents.length}) + + {documents.length > 0 ? ( +
+ {documents.map(doc => ( +
+
+ toggleDocumentSelection(doc.id)} + className="w-4 h-4 text-[#0476D9] rounded focus:ring-2 focus:ring-[#0476D9]" + /> + +
+

{doc.name}

+

+ {doc.type} • {doc.file_size} + {doc.notes && ` • ${doc.notes}`} +

+
+
+
+ + View + + {isAdmin() && ( + + )}
-
- - View - - {isAdmin() && ( - - )} -
-
- ))} -
- ) : ( -

No documents attached yet

- )} - {canWrite() && ( - - )} -
- )} -
- ); - })} + ))} +
+ ) : ( +

No documents attached yet

+ )} + {canWrite() && ( + + )} +
+ )} +
+ ); + })} +
-
+ )}
- ))} + ); + })}
)}