diff --git a/Ivanti_config_template.ini b/Ivanti_config_template.ini new file mode 100644 index 0000000..8f42260 --- /dev/null +++ b/Ivanti_config_template.ini @@ -0,0 +1,7 @@ +[platform] + url = https://platform4.risksense.com + api_ver = /api/v1 + # PROD 1550 | UAT 1551 + client_id = +[secrets] + api_key = diff --git a/architecture.excalidraw b/architecture.excalidraw new file mode 100644 index 0000000..0589209 --- /dev/null +++ b/architecture.excalidraw @@ -0,0 +1,838 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "title-text", + "type": "text", + "x": 400, + "y": 30, + "width": 400, + "height": 45, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "CVE Dashboard Architecture", + "fontSize": 36, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 32, + "containerId": null, + "originalText": "CVE Dashboard Architecture" + }, + { + "id": "users-box", + "type": "ellipse", + "x": 500, + "y": 120, + "width": 200, + "height": 80, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#e7f5ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "users-text" + }, + { + "id": "arrow-users-frontend", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "users-text", + "type": "text", + "x": 505, + "y": 145, + "width": 190, + "height": 30, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 3, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Users\n(Admin/Editor/Viewer)", + "fontSize": 16, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 23, + "containerId": "users-box", + "originalText": "Users\n(Admin/Editor/Viewer)" + }, + { + "id": "frontend-box", + "type": "rectangle", + "x": 450, + "y": 250, + "width": 300, + "height": 120, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "#a5d8ff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 4, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "frontend-text" + }, + { + "id": "arrow-users-frontend", + "type": "arrow" + }, + { + "id": "arrow-frontend-backend", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "frontend-text", + "type": "text", + "x": 455, + "y": 255, + "width": 290, + "height": 110, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 5, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Frontend (React)\nPort: 3000\n\n• React 18 + Tailwind CSS\n• Auth Context\n• Components: Login, UserMenu,\n UserManagement, CVE Views", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "middle", + "baseline": 103, + "containerId": "frontend-box", + "originalText": "Frontend (React)\nPort: 3000\n\n• React 18 + Tailwind CSS\n• Auth Context\n• Components: Login, UserMenu,\n UserManagement, CVE Views" + }, + { + "id": "backend-box", + "type": "rectangle", + "x": 400, + "y": 420, + "width": 400, + "height": 180, + "angle": 0, + "strokeColor": "#7048e8", + "backgroundColor": "#d0bfff", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 6, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "backend-text" + }, + { + "id": "arrow-frontend-backend", + "type": "arrow" + }, + { + "id": "arrow-backend-db", + "type": "arrow" + }, + { + "id": "arrow-backend-storage", + "type": "arrow" + }, + { + "id": "arrow-backend-nvd", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "backend-text", + "type": "text", + "x": 405, + "y": 425, + "width": 390, + "height": 170, + "angle": 0, + "strokeColor": "#7048e8", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 7, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Backend API (Express.js)\nPort: 3001\n\nRoutes:\n• /api/auth - Authentication (login/logout)\n• /api/users - User management\n• /api/cves - CVE operations\n• /api/documents - Document upload/download\n• /api/audit-log - Audit logging\n• /api/nvd-lookup - NVD integration\n• /api/weekly-reports - Weekly reports", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "middle", + "baseline": 163, + "containerId": "backend-box", + "originalText": "Backend API (Express.js)\nPort: 3001\n\nRoutes:\n• /api/auth - Authentication (login/logout)\n• /api/users - User management\n• /api/cves - CVE operations\n• /api/documents - Document upload/download\n• /api/audit-log - Audit logging\n• /api/nvd-lookup - NVD integration\n• /api/weekly-reports - Weekly reports" + }, + { + "id": "db-box", + "type": "rectangle", + "x": 200, + "y": 680, + "width": 280, + "height": 140, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "#b2f2bb", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 8, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "db-text" + }, + { + "id": "arrow-backend-db", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "db-text", + "type": "text", + "x": 205, + "y": 685, + "width": 270, + "height": 130, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 9, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "SQLite Database\ncve_database.db\n\nTables:\n• cves\n• documents\n• users\n• sessions\n• required_documents\n• audit_log", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "middle", + "baseline": 123, + "containerId": "db-box", + "originalText": "SQLite Database\ncve_database.db\n\nTables:\n• cves\n• documents\n• users\n• sessions\n• required_documents\n• audit_log" + }, + { + "id": "storage-box", + "type": "rectangle", + "x": 550, + "y": 680, + "width": 280, + "height": 140, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "#ffec99", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 10, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "storage-text" + }, + { + "id": "arrow-backend-storage", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "storage-text", + "type": "text", + "x": 555, + "y": 685, + "width": 270, + "height": 130, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 11, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "File Storage\nuploads/\n\nStructure:\nCVE-ID/\n Vendor/\n documents.pdf\n\n• Multi-vendor support\n• Timestamped filenames", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "middle", + "baseline": 123, + "containerId": "storage-box", + "originalText": "File Storage\nuploads/\n\nStructure:\nCVE-ID/\n Vendor/\n documents.pdf\n\n• Multi-vendor support\n• Timestamped filenames" + }, + { + "id": "nvd-box", + "type": "rectangle", + "x": 900, + "y": 420, + "width": 220, + "height": 100, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "#ffc9c9", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 12, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "nvd-text" + }, + { + "id": "arrow-backend-nvd", + "type": "arrow" + } + ], + "updated": 1, + "link": null, + "locked": false + }, + { + "id": "nvd-text", + "type": "text", + "x": 905, + "y": 425, + "width": 210, + "height": 90, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 13, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "NVD API\n(External)\n\nNational Vulnerability\nDatabase\n\nAutomatic CVE lookup", + "fontSize": 14, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "middle", + "baseline": 83, + "containerId": "nvd-box", + "originalText": "NVD API\n(External)\n\nNational Vulnerability\nDatabase\n\nAutomatic CVE lookup" + }, + { + "id": "arrow-users-frontend", + "type": "arrow", + "x": 600, + "y": 200, + "width": 0, + "height": 50, + "angle": 0, + "strokeColor": "#1971c2", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 14, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "points": [ + [0, 0], + [0, 50] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "users-box", + "focus": 0, + "gap": 1 + }, + "endBinding": { + "elementId": "frontend-box", + "focus": 0, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false, + "roundness": null + }, + { + "id": "arrow-frontend-backend", + "type": "arrow", + "x": 600, + "y": 370, + "width": 0, + "height": 50, + "angle": 0, + "strokeColor": "#7048e8", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 15, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "points": [ + [0, 0], + [0, 50] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "frontend-box", + "focus": 0, + "gap": 1 + }, + "endBinding": { + "elementId": "backend-box", + "focus": 0, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false, + "roundness": null + }, + { + "id": "arrow-backend-db", + "type": "arrow", + "x": 500, + "y": 600, + "width": -140, + "height": 80, + "angle": 0, + "strokeColor": "#2f9e44", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 16, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "points": [ + [0, 0], + [-140, 0], + [-140, 80] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "backend-box", + "focus": 0, + "gap": 1 + }, + "endBinding": { + "elementId": "db-box", + "focus": 0, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": true, + "roundness": null + }, + { + "id": "arrow-backend-storage", + "type": "arrow", + "x": 700, + "y": 600, + "width": 0, + "height": 80, + "angle": 0, + "strokeColor": "#f08c00", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 17, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "points": [ + [0, 0], + [0, 80] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "backend-box", + "focus": 0.5, + "gap": 1 + }, + "endBinding": { + "elementId": "storage-box", + "focus": 0.5, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false, + "roundness": null + }, + { + "id": "arrow-backend-nvd", + "type": "arrow", + "x": 800, + "y": 480, + "width": 100, + "height": 0, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 18, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "points": [ + [0, 0], + [100, 0] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "backend-box", + "focus": 0, + "gap": 1 + }, + "endBinding": { + "elementId": "nvd-box", + "focus": 0, + "gap": 1 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false, + "roundness": null + }, + { + "id": "label-http", + "type": "text", + "x": 610, + "y": 390, + "width": 100, + "height": 20, + "angle": 0, + "strokeColor": "#7048e8", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 19, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "HTTP/REST API", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 17, + "containerId": null, + "originalText": "HTTP/REST API" + }, + { + "id": "label-https", + "type": "text", + "x": 820, + "y": 460, + "width": 60, + "height": 20, + "angle": 0, + "strokeColor": "#e03131", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 20, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "HTTPS", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 17, + "containerId": null, + "originalText": "HTTPS" + }, + { + "id": "auth-note", + "type": "text", + "x": 100, + "y": 250, + "width": 280, + "height": 80, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#f8f9fa", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 21, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Authentication:\n• Session-based auth\n• bcrypt password hashing\n• Role-based access control\n (Admin/Editor/Viewer)", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 73, + "containerId": null, + "originalText": "Authentication:\n• Session-based auth\n• bcrypt password hashing\n• Role-based access control\n (Admin/Editor/Viewer)" + }, + { + "id": "features-note", + "type": "text", + "x": 900, + "y": 580, + "width": 280, + "height": 120, + "angle": 0, + "strokeColor": "#495057", + "backgroundColor": "#f8f9fa", + "fillStyle": "solid", + "strokeWidth": 1, + "strokeStyle": "dashed", + "roughness": 0, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 22, + "version": 1, + "versionNonce": 1, + "isDeleted": false, + "boundElements": null, + "updated": 1, + "link": null, + "locked": false, + "text": "Key Features:\n• Quick CVE status check\n• Multi-vendor support\n• Document management\n• Compliance tracking\n• Search & filter\n• Weekly report uploads\n• Audit logging", + "fontSize": 12, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 113, + "containerId": null, + "originalText": "Key Features:\n• Quick CVE status check\n• Multi-vendor support\n• Document management\n• Compliance tracking\n• Search & filter\n• Weekly report uploads\n• Audit logging" + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} diff --git a/backend/migrations/add_knowledge_base_table.js b/backend/migrations/add_knowledge_base_table.js new file mode 100644 index 0000000..395487a --- /dev/null +++ b/backend/migrations/add_knowledge_base_table.js @@ -0,0 +1,70 @@ +// Migration: Add knowledge_base table for storing documentation and policies + +const sqlite3 = require('sqlite3').verbose(); +const path = require('path'); + +const dbPath = path.join(__dirname, '..', 'cve_database.db'); +const db = new sqlite3.Database(dbPath); + +console.log('Running migration: add_knowledge_base_table'); + +db.serialize(() => { + db.run(` + CREATE TABLE IF NOT EXISTS knowledge_base ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title VARCHAR(255) NOT NULL, + slug VARCHAR(255) UNIQUE NOT NULL, + description TEXT, + category VARCHAR(100), + file_path VARCHAR(500), + file_name VARCHAR(255), + file_type VARCHAR(50), + file_size INTEGER, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by INTEGER, + FOREIGN KEY (created_by) REFERENCES users(id) + ) + `, (err) => { + if (err) { + console.error('Error creating knowledge_base table:', err); + process.exit(1); + } + console.log('✓ Created knowledge_base table'); + }); + + db.run(` + CREATE INDEX IF NOT EXISTS idx_knowledge_base_slug + ON knowledge_base(slug) + `, (err) => { + if (err) { + console.error('Error creating slug index:', err); + process.exit(1); + } + console.log('✓ Created index on slug'); + }); + + db.run(` + CREATE INDEX IF NOT EXISTS idx_knowledge_base_category + ON knowledge_base(category) + `, (err) => { + if (err) { + console.error('Error creating category index:', err); + process.exit(1); + } + console.log('✓ Created index on category'); + }); + + db.run(` + CREATE INDEX IF NOT EXISTS idx_knowledge_base_created_at + ON knowledge_base(created_at DESC) + `, (err) => { + if (err) { + console.error('Error creating created_at index:', err); + process.exit(1); + } + console.log('✓ Created index on created_at'); + console.log('\nMigration completed successfully!'); + db.close(); + }); +}); diff --git a/backend/routes/knowledgeBase.js b/backend/routes/knowledgeBase.js new file mode 100644 index 0000000..2898714 --- /dev/null +++ b/backend/routes/knowledgeBase.js @@ -0,0 +1,334 @@ +const express = require('express'); +const path = require('path'); +const fs = require('fs'); +const { requireAuth, requireRole } = require('../middleware/auth'); +const logAudit = require('../helpers/auditLog'); + +function createKnowledgeBaseRouter(db, upload) { + const router = express.Router(); + + // Helper to sanitize filename + function sanitizePathSegment(segment) { + if (!segment || typeof segment !== 'string') return ''; + return segment + .replace(/\0/g, '') + .replace(/\.\./g, '') + .replace(/[\/\\]/g, '') + .trim(); + } + + // Helper to generate slug from title + function generateSlug(title) { + return title + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, '') + .substring(0, 200); + } + + // Helper to validate file type + const ALLOWED_EXTENSIONS = new Set([ + '.pdf', '.md', '.txt', '.doc', '.docx', + '.xls', '.xlsx', '.ppt', '.pptx', + '.html', '.htm', '.json', '.yaml', '.yml', + '.png', '.jpg', '.jpeg', '.gif' + ]); + + function isValidFileType(filename) { + const ext = path.extname(filename).toLowerCase(); + return ALLOWED_EXTENSIONS.has(ext); + } + + // POST /api/knowledge-base/upload - Upload new document + router.post('/upload', requireAuth(db), requireRole(db, 'editor', 'admin'), upload.single('file'), async (req, res) => { + const uploadedFile = req.file; + const { title, description, category } = req.body; + + // Validate required fields + if (!title || !title.trim()) { + if (uploadedFile) fs.unlinkSync(uploadedFile.path); + return res.status(400).json({ error: 'Title is required' }); + } + + if (!uploadedFile) { + return res.status(400).json({ error: 'No file uploaded' }); + } + + // Validate file type + if (!isValidFileType(uploadedFile.originalname)) { + fs.unlinkSync(uploadedFile.path); + return res.status(400).json({ error: 'File type not allowed' }); + } + + const timestamp = Date.now(); + const sanitizedName = sanitizePathSegment(uploadedFile.originalname); + const slug = generateSlug(title); + const kbDir = path.join(__dirname, '..', 'uploads', 'knowledge_base'); + + // Create directory if it doesn't exist + if (!fs.existsSync(kbDir)) { + fs.mkdirSync(kbDir, { recursive: true }); + } + + const filename = `${timestamp}_${sanitizedName}`; + const filePath = path.join(kbDir, filename); + + try { + // Move uploaded file to permanent location + fs.renameSync(uploadedFile.path, filePath); + + // Check if slug already exists + db.get('SELECT id FROM knowledge_base WHERE slug = ?', [slug], (err, row) => { + if (err) { + fs.unlinkSync(filePath); + console.error('Error checking slug:', err); + return res.status(500).json({ error: 'Database error' }); + } + + // If slug exists, append timestamp to make it unique + const finalSlug = row ? `${slug}-${timestamp}` : slug; + + // Insert new knowledge base entry + const insertSql = ` + INSERT INTO knowledge_base ( + title, slug, description, category, file_path, file_name, + file_type, file_size, created_by + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `; + + db.run( + insertSql, + [ + title.trim(), + finalSlug, + description || null, + category || 'General', + filePath, + sanitizedName, + uploadedFile.mimetype, + uploadedFile.size, + req.user.id + ], + function (err) { + if (err) { + fs.unlinkSync(filePath); + console.error('Error inserting knowledge base entry:', err); + return res.status(500).json({ error: 'Failed to save document metadata' }); + } + + // Log audit entry + logAudit( + db, + req.user.id, + req.user.username, + 'CREATE_KB_ARTICLE', + 'knowledge_base', + this.lastID, + JSON.stringify({ title: title.trim(), filename: sanitizedName }), + req.ip + ); + + res.json({ + success: true, + id: this.lastID, + title: title.trim(), + slug: finalSlug, + category: category || 'General' + }); + } + ); + }); + } catch (error) { + // Clean up file on error + if (fs.existsSync(filePath)) fs.unlinkSync(filePath); + console.error('Error uploading knowledge base document:', error); + res.status(500).json({ error: error.message || 'Failed to upload document' }); + } + }); + + // GET /api/knowledge-base - List all articles + router.get('/', requireAuth(db), (req, res) => { + const sql = ` + SELECT + kb.id, kb.title, kb.slug, kb.description, kb.category, + kb.file_name, kb.file_type, kb.file_size, kb.created_at, kb.updated_at, + u.username as created_by_username + FROM knowledge_base kb + LEFT JOIN users u ON kb.created_by = u.id + ORDER BY kb.created_at DESC + `; + + db.all(sql, [], (err, rows) => { + if (err) { + console.error('Error fetching knowledge base articles:', err); + return res.status(500).json({ error: 'Failed to fetch articles' }); + } + + res.json(rows); + }); + }); + + // GET /api/knowledge-base/:id - Get single article details + router.get('/:id', requireAuth(db), (req, res) => { + const { id } = req.params; + + const sql = ` + SELECT + kb.id, kb.title, kb.slug, kb.description, kb.category, + kb.file_name, kb.file_type, kb.file_size, kb.created_at, kb.updated_at, + u.username as created_by_username + FROM knowledge_base kb + LEFT JOIN users u ON kb.created_by = u.id + WHERE kb.id = ? + `; + + db.get(sql, [id], (err, row) => { + if (err) { + console.error('Error fetching article:', err); + return res.status(500).json({ error: 'Failed to fetch article' }); + } + + if (!row) { + return res.status(404).json({ error: 'Article not found' }); + } + + res.json(row); + }); + }); + + // GET /api/knowledge-base/:id/content - Get document content for display + router.get('/:id/content', requireAuth(db), (req, res) => { + const { id } = req.params; + + const sql = 'SELECT file_path, file_name, file_type FROM knowledge_base WHERE id = ?'; + + db.get(sql, [id], (err, row) => { + if (err) { + console.error('Error fetching document:', err); + return res.status(500).json({ error: 'Failed to fetch document' }); + } + + if (!row) { + return res.status(404).json({ error: 'Document not found' }); + } + + if (!fs.existsSync(row.file_path)) { + return res.status(404).json({ error: 'File not found on disk' }); + } + + // Log audit entry + logAudit( + db, + req.user.id, + req.user.username, + 'VIEW_KB_ARTICLE', + 'knowledge_base', + id, + JSON.stringify({ filename: row.file_name }), + req.ip + ); + + // Determine content type for inline display + let contentType = row.file_type || 'application/octet-stream'; + + // For markdown files, send as plain text so frontend can parse it + if (row.file_name.endsWith('.md')) { + contentType = 'text/plain; charset=utf-8'; + } else if (row.file_name.endsWith('.txt')) { + contentType = 'text/plain; charset=utf-8'; + } + + res.setHeader('Content-Type', contentType); + // Use inline instead of attachment to allow browser to display + res.setHeader('Content-Disposition', `inline; filename="${row.file_name}"`); + res.sendFile(row.file_path); + }); + }); + + // GET /api/knowledge-base/:id/download - Download document + router.get('/:id/download', requireAuth(db), (req, res) => { + const { id } = req.params; + + const sql = 'SELECT file_path, file_name, file_type FROM knowledge_base WHERE id = ?'; + + db.get(sql, [id], (err, row) => { + if (err) { + console.error('Error fetching document:', err); + return res.status(500).json({ error: 'Failed to fetch document' }); + } + + if (!row) { + return res.status(404).json({ error: 'Document not found' }); + } + + if (!fs.existsSync(row.file_path)) { + return res.status(404).json({ error: 'File not found on disk' }); + } + + // Log audit entry + logAudit( + db, + req.user.id, + req.user.username, + 'DOWNLOAD_KB_ARTICLE', + 'knowledge_base', + id, + JSON.stringify({ filename: row.file_name }), + req.ip + ); + + res.setHeader('Content-Type', row.file_type || 'application/octet-stream'); + res.setHeader('Content-Disposition', `attachment; filename="${row.file_name}"`); + res.sendFile(row.file_path); + }); + }); + + // DELETE /api/knowledge-base/:id - Delete article + router.delete('/:id', requireAuth(db), requireRole(db, 'editor', 'admin'), (req, res) => { + const { id } = req.params; + + const sql = 'SELECT file_path, title FROM knowledge_base WHERE id = ?'; + + db.get(sql, [id], (err, row) => { + if (err) { + console.error('Error fetching article for deletion:', err); + return res.status(500).json({ error: 'Failed to fetch article' }); + } + + if (!row) { + return res.status(404).json({ error: 'Article not found' }); + } + + // Delete database record + db.run('DELETE FROM knowledge_base WHERE id = ?', [id], (err) => { + if (err) { + console.error('Error deleting article:', err); + return res.status(500).json({ error: 'Failed to delete article' }); + } + + // Delete file + if (fs.existsSync(row.file_path)) { + fs.unlinkSync(row.file_path); + } + + // Log audit entry + logAudit( + db, + req.user.id, + req.user.username, + 'DELETE_KB_ARTICLE', + 'knowledge_base', + id, + JSON.stringify({ title: row.title }), + req.ip + ); + + res.json({ success: true }); + }); + }); + }); + + return router; +} + +module.exports = createKnowledgeBaseRouter; diff --git a/backend/server.js b/backend/server.js index f139057..bd459bf 100644 --- a/backend/server.js +++ b/backend/server.js @@ -19,6 +19,7 @@ const createAuditLogRouter = require('./routes/auditLog'); const logAudit = require('./helpers/auditLog'); const createNvdLookupRouter = require('./routes/nvdLookup'); const createWeeklyReportsRouter = require('./routes/weeklyReports'); +const createKnowledgeBaseRouter = require('./routes/knowledgeBase'); const app = express(); const PORT = process.env.PORT || 3001; @@ -171,6 +172,9 @@ const upload = multer({ // Weekly reports routes (editor/admin for upload, all authenticated for download) app.use('/api/weekly-reports', createWeeklyReportsRouter(db, upload)); +// Knowledge base routes (editor/admin for upload/delete, all authenticated for view) +app.use('/api/knowledge-base', createKnowledgeBaseRouter(db, upload)); + // ========== CVE ENDPOINTS ========== // Get all CVEs with optional filters (authenticated users) diff --git a/frontend/package.json b/frontend/package.json index b96622d..324b36d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,6 +10,7 @@ "lucide-react": "^0.563.0", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-markdown": "^10.1.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" }, diff --git a/frontend/src/App.css b/frontend/src/App.css index 4194d79..38c9bcc 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -647,3 +647,179 @@ h3.text-intel-accent { inset 0 2px 4px rgba(0, 0, 0, 0.25), 0 2px 8px rgba(14, 165, 233, 0.1); } + +/* Knowledge Base Content Area */ +.kb-content-area { + min-height: 400px; + max-height: 700px; + overflow-y: auto; + padding-right: 0.5rem; +} + +/* Markdown Content Styling */ +.markdown-content { + color: #E2E8F0; + line-height: 1.7; + font-size: 0.95rem; +} + +.markdown-content h1 { + font-size: 2rem; + font-weight: 700; + color: #0EA5E9; + margin-top: 1.5rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid rgba(14, 165, 233, 0.3); + font-family: monospace; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.markdown-content h2 { + font-size: 1.5rem; + font-weight: 600; + color: #10B981; + margin-top: 1.5rem; + margin-bottom: 0.75rem; + font-family: monospace; +} + +.markdown-content h3 { + font-size: 1.25rem; + font-weight: 600; + color: #F59E0B; + margin-top: 1.25rem; + margin-bottom: 0.5rem; + font-family: monospace; +} + +.markdown-content h4, +.markdown-content h5, +.markdown-content h6 { + font-size: 1.1rem; + font-weight: 600; + color: #94A3B8; + margin-top: 1rem; + margin-bottom: 0.5rem; +} + +.markdown-content p { + margin-bottom: 1rem; + color: #CBD5E1; +} + +.markdown-content a { + color: #0EA5E9; + text-decoration: none; + border-bottom: 1px solid rgba(14, 165, 233, 0.3); + transition: all 0.2s; +} + +.markdown-content a:hover { + color: #38BDF8; + border-bottom-color: #38BDF8; +} + +.markdown-content ul, +.markdown-content ol { + margin-bottom: 1rem; + padding-left: 1.5rem; + color: #CBD5E1; +} + +.markdown-content li { + margin-bottom: 0.5rem; +} + +.markdown-content code { + background: rgba(15, 23, 42, 0.8); + border: 1px solid rgba(14, 165, 233, 0.2); + border-radius: 0.25rem; + padding: 0.125rem 0.375rem; + font-family: 'Courier New', monospace; + font-size: 0.9em; + color: #10B981; +} + +.markdown-content pre { + background: rgba(15, 23, 42, 0.95); + border: 1px solid rgba(14, 165, 233, 0.3); + border-radius: 0.5rem; + padding: 1rem; + margin-bottom: 1rem; + overflow-x: auto; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3); +} + +.markdown-content pre code { + background: none; + border: none; + padding: 0; + color: #E2E8F0; + font-size: 0.875rem; +} + +.markdown-content blockquote { + border-left: 4px solid #0EA5E9; + padding-left: 1rem; + margin: 1rem 0; + color: #94A3B8; + font-style: italic; + background: rgba(14, 165, 233, 0.05); + padding: 0.75rem 1rem; + border-radius: 0.25rem; +} + +.markdown-content table { + width: 100%; + border-collapse: collapse; + margin-bottom: 1rem; + font-size: 0.9rem; +} + +.markdown-content th, +.markdown-content td { + border: 1px solid rgba(14, 165, 233, 0.2); + padding: 0.5rem 0.75rem; + text-align: left; +} + +.markdown-content th { + background: rgba(14, 165, 233, 0.1); + color: #0EA5E9; + font-weight: 600; + font-family: monospace; +} + +.markdown-content td { + color: #CBD5E1; +} + +.markdown-content tr:hover { + background: rgba(14, 165, 233, 0.05); +} + +.markdown-content hr { + border: none; + border-top: 1px solid rgba(14, 165, 233, 0.2); + margin: 2rem 0; +} + +.markdown-content img { + max-width: 100%; + height: auto; + border-radius: 0.5rem; + border: 1px solid rgba(14, 165, 233, 0.3); + margin: 1rem 0; +} + +.markdown-content strong { + color: #F8FAFC; + font-weight: 600; +} + +.markdown-content em { + color: #CBD5E1; + font-style: italic; +} diff --git a/frontend/src/App.js b/frontend/src/App.js index c16d547..43eee22 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -7,6 +7,8 @@ import UserManagement from './components/UserManagement'; import AuditLog from './components/AuditLog'; import NvdSyncModal from './components/NvdSyncModal'; import WeeklyReportModal from './components/WeeklyReportModal'; +import KnowledgeBaseModal from './components/KnowledgeBaseModal'; +import KnowledgeBaseViewer from './components/KnowledgeBaseViewer'; import './App.css'; const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; @@ -175,6 +177,9 @@ export default function App() { const [showAuditLog, setShowAuditLog] = useState(false); const [showNvdSync, setShowNvdSync] = useState(false); const [showWeeklyReport, setShowWeeklyReport] = useState(false); + const [showKnowledgeBase, setShowKnowledgeBase] = useState(false); + const [knowledgeBaseArticles, setKnowledgeBaseArticles] = useState([]); + const [selectedKBArticle, setSelectedKBArticle] = useState(null); const [newCVE, setNewCVE] = useState({ cve_id: '', vendor: '', @@ -278,6 +283,19 @@ export default function App() { } }; + const fetchKnowledgeBaseArticles = async () => { + try { + const response = await fetch(`${API_BASE}/knowledge-base`, { + credentials: 'include' + }); + if (!response.ok) throw new Error('Failed to fetch knowledge base articles'); + const data = await response.json(); + setKnowledgeBaseArticles(data); + } catch (err) { + console.error('Error fetching knowledge base articles:', err); + } + }; + const fetchJiraTickets = async () => { try { const response = await fetch(`${API_BASE}/jira-tickets`, { @@ -346,6 +364,45 @@ export default function App() { alert(`Exporting ${selectedDocuments.length} documents for report attachment`); }; + const handleViewKBArticle = async (articleId) => { + try { + const response = await fetch(`${API_BASE}/knowledge-base/${articleId}`, { + credentials: 'include' + }); + + if (!response.ok) throw new Error('Failed to fetch article'); + + const article = await response.json(); + setSelectedKBArticle(article); + } catch (err) { + console.error('Error fetching knowledge base article:', err); + setError('Failed to load article'); + } + }; + + const handleDownloadKBArticle = async (id, filename) => { + try { + const response = await fetch(`${API_BASE}/knowledge-base/${id}/download`, { + credentials: 'include' + }); + + if (!response.ok) throw new Error('Download failed'); + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (err) { + console.error('Error downloading knowledge base article:', err); + setError('Failed to download document'); + } + }; + const handleAddCVE = async (e) => { e.preventDefault(); try { @@ -694,6 +751,7 @@ export default function App() { fetchCVEs(); fetchVendors(); fetchJiraTickets(); + fetchKnowledgeBaseArticles(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isAuthenticated]); @@ -826,6 +884,14 @@ export default function App() { setShowWeeklyReport(false)} /> )} + {/* Knowledge Base Modal */} + {showKnowledgeBase && ( + setShowKnowledgeBase(false)} + onUpdate={fetchKnowledgeBaseArticles} + /> + )} + {/* Add CVE Modal */} {showAddCVE && (
@@ -1276,47 +1342,85 @@ export default function App() { {/* LEFT PANEL - Wiki/Knowledge Base */}
-

- Knowledge Base -

+
+

+ Knowledge Base +

+ {(user?.role === 'admin' || user?.role === 'editor') && ( + + )} +
- {/* Wiki/Blog Style Entries */} + {/* Knowledge Base Entries */}
-
-

CVE Response Procedures

-

Standard operating procedures for vulnerability response and escalation...

- Last updated: 2024-02-08 -
- -
-

Vendor Contact Matrix

-

Emergency contacts and escalation paths for security vendors...

- Last updated: 2024-02-05 -
- -
-

Severity Classification Guide

-

Guidelines for assessing and classifying vulnerability severity levels...

- Last updated: 2024-01-28 -
- -
-

Patching Policy

-

Enterprise patch management timelines and approval workflow...

- Last updated: 2024-01-15 -
- -
-

Documentation Standards

-

Required documentation for vulnerability tracking and audit compliance...

- Last updated: 2024-01-10 -
+ {knowledgeBaseArticles.length === 0 ? ( +
+ +

No documents yet

+ {(user?.role === 'admin' || user?.role === 'editor') && ( + + )} +
+ ) : ( + knowledgeBaseArticles.slice(0, 5).map((article) => ( +
handleViewKBArticle(article.id)} + style={{ background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.85) 0%, rgba(51, 65, 85, 0.75) 100%)', border: '1px solid rgba(16, 185, 129, 0.25)', borderRadius: '0.375rem', padding: '0.75rem', cursor: 'pointer', transition: 'all 0.2s' }} + className="hover:border-intel-success" + > +

{article.title}

+ {article.description && ( +

{article.description}

+ )} +
+ + {new Date(article.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })} + + {article.category && article.category !== 'General' && ( + + {article.category} + + )} +
+
+ )) + )} + {knowledgeBaseArticles.length > 5 && ( + + )}
{/* CENTER PANEL - Main Content */}
+ {/* Knowledge Base Viewer */} + {selectedKBArticle ? ( + setSelectedKBArticle(null)} + /> + ) : ( + <> {/* Quick Check */}
@@ -1753,6 +1857,8 @@ export default function App() {

Try adjusting your search criteria or filters

)} + + )}
{/* End Center Panel */} diff --git a/frontend/src/components/KnowledgeBaseModal.js b/frontend/src/components/KnowledgeBaseModal.js new file mode 100644 index 0000000..7e6ee05 --- /dev/null +++ b/frontend/src/components/KnowledgeBaseModal.js @@ -0,0 +1,384 @@ +import React, { useState, useEffect } from 'react'; +import { X, Loader, AlertCircle, CheckCircle, Upload as UploadIcon, Download, FileText, Trash2 } from 'lucide-react'; + +const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api'; + +export default function KnowledgeBaseModal({ onClose, onUpdate }) { + const [phase, setPhase] = useState('idle'); // idle, uploading, success, error + const [selectedFile, setSelectedFile] = useState(null); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [category, setCategory] = useState('General'); + const [result, setResult] = useState(null); + const [existingArticles, setExistingArticles] = useState([]); + const [error, setError] = useState(''); + + // Fetch existing articles on mount + useEffect(() => { + fetchExistingArticles(); + }, []); + + const fetchExistingArticles = async () => { + try { + const response = await fetch(`${API_BASE}/knowledge-base`, { credentials: 'include' }); + if (!response.ok) throw new Error('Failed to fetch articles'); + const data = await response.json(); + setExistingArticles(data); + } catch (err) { + console.error('Error fetching articles:', err); + } + }; + + const handleFileSelect = (e) => { + const file = e.target.files[0]; + if (file) { + // Validate file type + const allowedExtensions = ['.pdf', '.md', '.txt', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.html', '.json', '.yaml', '.yml']; + const ext = '.' + file.name.split('.').pop().toLowerCase(); + + if (!allowedExtensions.includes(ext)) { + setError('File type not allowed. Please upload: PDF, Markdown, Text, Office docs, or HTML files.'); + return; + } + + setSelectedFile(file); + setError(''); + + // Auto-populate title from filename if empty + if (!title) { + const filename = file.name.replace(/\.[^/.]+$/, ''); // Remove extension + setTitle(filename); + } + } + }; + + const handleUpload = async () => { + if (!selectedFile || !title.trim()) { + setError('Please provide both a title and file'); + return; + } + + setPhase('uploading'); + + const formData = new FormData(); + formData.append('file', selectedFile); + formData.append('title', title.trim()); + formData.append('description', description.trim()); + formData.append('category', category); + + try { + const response = await fetch(`${API_BASE}/knowledge-base/upload`, { + method: 'POST', + body: formData, + credentials: 'include' + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || 'Upload failed'); + } + + const data = await response.json(); + setResult(data); + setPhase('success'); + + // Refresh the list of existing articles + await fetchExistingArticles(); + + // Notify parent to refresh + if (onUpdate) onUpdate(); + } catch (err) { + setError(err.message); + setPhase('error'); + } + }; + + const handleDownload = async (id, filename) => { + try { + const response = await fetch(`${API_BASE}/knowledge-base/${id}/download`, { + credentials: 'include' + }); + + if (!response.ok) throw new Error('Download failed'); + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (err) { + console.error('Error downloading file:', err); + setError('Failed to download file'); + } + }; + + const handleDelete = async (id, articleTitle) => { + if (!window.confirm(`Are you sure you want to delete "${articleTitle}"?`)) { + return; + } + + try { + const response = await fetch(`${API_BASE}/knowledge-base/${id}`, { + method: 'DELETE', + credentials: 'include' + }); + + if (!response.ok) throw new Error('Delete failed'); + + // Refresh the list + await fetchExistingArticles(); + + // Notify parent to refresh + if (onUpdate) onUpdate(); + } catch (err) { + console.error('Error deleting article:', err); + setError('Failed to delete article'); + } + }; + + const resetForm = () => { + setPhase('idle'); + setSelectedFile(null); + setTitle(''); + setDescription(''); + setCategory('General'); + setResult(null); + setError(''); + }; + + const formatFileSize = (bytes) => { + if (!bytes) return 'Unknown size'; + if (bytes < 1024) return bytes + ' B'; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; + return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; + }; + + const formatDate = (dateString) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric' + }); + }; + + const getCategoryColor = (cat) => { + const colors = { + 'General': '#94A3B8', + 'Policy': '#0EA5E9', + 'Procedure': '#10B981', + 'Guide': '#F59E0B', + 'Reference': '#8B5CF6' + }; + return colors[cat] || '#94A3B8'; + }; + + return ( +
+
e.stopPropagation()} style={{ maxWidth: '700px' }}> + {/* Header */} +
+

Knowledge Base

+ +
+ + {/* Body */} +
+ {/* Idle Phase - Upload Form */} + {phase === 'idle' && ( +
+
+ + setTitle(e.target.value)} + placeholder="e.g., Inventory Management Policy" + className="intel-input w-full" + maxLength={255} + /> +
+ +
+ +