8 Commits

11 changed files with 2382 additions and 37 deletions

View File

@@ -0,0 +1,7 @@
[platform]
url = https://platform4.risksense.com
api_ver = /api/v1
# PROD 1550 | UAT 1551
client_id = <pick 1550 or 1551>
[secrets]
api_key = <your API key here>

838
architecture.excalidraw Normal file
View File

@@ -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": {}
}

View File

@@ -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();
});
});

View File

@@ -0,0 +1,352 @@
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'), (req, res, next) => {
upload.single('file')(req, res, (err) => {
if (err) {
console.error('[KB Upload] Multer error:', err);
return res.status(400).json({ error: err.message || 'File upload failed' });
}
next();
});
}, async (req, res) => {
console.log('[KB Upload] Request received:', {
hasFile: !!req.file,
body: req.body,
contentType: req.headers['content-type']
});
const uploadedFile = req.file;
const { title, description, category } = req.body;
// Validate required fields
if (!title || !title.trim()) {
console.error('[KB Upload] Error: Title is missing');
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}"`);
// Allow iframe embedding from frontend origin
res.removeHeader('X-Frame-Options');
res.setHeader('Content-Security-Policy', "frame-ancestors 'self' http://71.85.90.9:3000 http://localhost:3000");
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;

View File

@@ -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;
@@ -33,7 +34,7 @@ const CORS_ORIGINS = process.env.CORS_ORIGINS
// Allowed file extensions for document uploads (documents only, no executables)
const ALLOWED_EXTENSIONS = new Set([
'.pdf', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.tiff', '.tif',
'.txt', '.csv', '.log', '.msg', '.eml',
'.txt', '.md', '.csv', '.log', '.msg', '.eml',
'.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.odt', '.ods', '.odp',
'.rtf', '.html', '.htm', '.xml', '.json', '.yaml', '.yml',
@@ -95,7 +96,7 @@ app.use((req, res, next) => {
// Security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Frame-Options', 'SAMEORIGIN'); // Allow iframes from same origin
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
@@ -107,7 +108,11 @@ app.use(cors({
origin: CORS_ORIGINS,
credentials: true
}));
app.use(express.json({ limit: '1mb' }));
// Only parse JSON for requests with application/json content type
app.use(express.json({
limit: '1mb',
type: 'application/json'
}));
app.use(cookieParser());
app.use('/uploads', express.static('uploads', {
dotfiles: 'deny',
@@ -171,6 +176,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)

View File

@@ -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"
},

View File

@@ -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;
}

View File

@@ -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';
@@ -156,7 +158,7 @@ const API_HOST = process.env.REACT_APP_API_HOST || 'http://localhost:3001';
const severityLevels = ['All Severities', 'Critical', 'High', 'Medium', 'Low'];
export default function App() {
const { isAuthenticated, loading: authLoading, canWrite, isAdmin } = useAuth();
const { isAuthenticated, loading: authLoading, canWrite, isAdmin, user } = useAuth();
const [searchQuery, setSearchQuery] = useState('');
const [selectedVendor, setSelectedVendor] = useState('All Vendors');
const [selectedSeverity, setSelectedSeverity] = useState('All Severities');
@@ -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() {
<WeeklyReportModal onClose={() => setShowWeeklyReport(false)} />
)}
{/* Knowledge Base Modal */}
{showKnowledgeBase && (
<KnowledgeBaseModal
onClose={() => setShowKnowledgeBase(false)}
onUpdate={fetchKnowledgeBaseArticles}
/>
)}
{/* Add CVE Modal */}
{showAddCVE && (
<div className="fixed inset-0 modal-overlay flex items-center justify-center z-50 p-4">
@@ -1276,47 +1342,85 @@ export default function App() {
{/* LEFT PANEL - Wiki/Knowledge Base */}
<div className="col-span-12 lg:col-span-3 space-y-4">
<div style={{...STYLES.intelCard, padding: '1.5rem', borderLeft: '3px solid #10B981'}} className="rounded-lg">
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#10B981', marginBottom: '1rem', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(16, 185, 129, 0.4)' }}>
<div className="flex items-center justify-between mb-4">
<h2 style={{ fontSize: '1.125rem', fontWeight: '600', color: '#10B981', marginBottom: '0', fontFamily: 'monospace', textTransform: 'uppercase', letterSpacing: '0.1em', textShadow: '0 0 12px rgba(16, 185, 129, 0.4)' }}>
Knowledge Base
</h2>
{(user?.role === 'admin' || user?.role === 'editor') && (
<button
onClick={() => setShowKnowledgeBase(true)}
className="intel-button intel-button-small"
style={{ fontSize: '0.75rem', padding: '0.375rem 0.75rem' }}
title="Manage Knowledge Base"
>
<Plus className="w-3 h-3" />
</button>
)}
</div>
{/* Wiki/Blog Style Entries */}
{/* Knowledge Base Entries */}
<div className="space-y-3">
<div 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">
<h3 className="text-white font-semibold text-sm mb-1 font-mono">CVE Response Procedures</h3>
<p className="text-gray-400 text-xs mb-2">Standard operating procedures for vulnerability response and escalation...</p>
<span className="text-xs text-intel-success font-mono">Last updated: 2024-02-08</span>
{knowledgeBaseArticles.length === 0 ? (
<div className="text-center py-8" style={{ color: '#64748B' }}>
<FileText className="w-12 h-12 mx-auto mb-2 opacity-50" />
<p className="text-sm">No documents yet</p>
{(user?.role === 'admin' || user?.role === 'editor') && (
<button
onClick={() => setShowKnowledgeBase(true)}
className="intel-button intel-button-small mt-3"
>
Add First Document
</button>
)}
</div>
<div 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">
<h3 className="text-white font-semibold text-sm mb-1 font-mono">Vendor Contact Matrix</h3>
<p className="text-gray-400 text-xs mb-2">Emergency contacts and escalation paths for security vendors...</p>
<span className="text-xs text-intel-success font-mono">Last updated: 2024-02-05</span>
) : (
knowledgeBaseArticles.slice(0, 5).map((article) => (
<div
key={article.id}
onClick={() => 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"
>
<h3 className="text-white font-semibold text-sm mb-1 font-mono">{article.title}</h3>
{article.description && (
<p className="text-gray-400 text-xs mb-2 line-clamp-2">{article.description}</p>
)}
<div className="flex items-center justify-between">
<span className="text-xs text-intel-success font-mono">
{new Date(article.created_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
</span>
{article.category && article.category !== 'General' && (
<span className="text-xs px-2 py-0.5 rounded" style={{ background: 'rgba(16, 185, 129, 0.2)', color: '#10B981' }}>
{article.category}
</span>
)}
</div>
<div 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">
<h3 className="text-white font-semibold text-sm mb-1 font-mono">Severity Classification Guide</h3>
<p className="text-gray-400 text-xs mb-2">Guidelines for assessing and classifying vulnerability severity levels...</p>
<span className="text-xs text-intel-success font-mono">Last updated: 2024-01-28</span>
</div>
<div 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">
<h3 className="text-white font-semibold text-sm mb-1 font-mono">Patching Policy</h3>
<p className="text-gray-400 text-xs mb-2">Enterprise patch management timelines and approval workflow...</p>
<span className="text-xs text-intel-success font-mono">Last updated: 2024-01-15</span>
</div>
<div 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">
<h3 className="text-white font-semibold text-sm mb-1 font-mono">Documentation Standards</h3>
<p className="text-gray-400 text-xs mb-2">Required documentation for vulnerability tracking and audit compliance...</p>
<span className="text-xs text-intel-success font-mono">Last updated: 2024-01-10</span>
</div>
))
)}
{knowledgeBaseArticles.length > 5 && (
<button
onClick={() => setShowKnowledgeBase(true)}
className="text-xs text-center w-full py-2"
style={{ color: '#10B981' }}
>
View all {knowledgeBaseArticles.length} documents
</button>
)}
</div>
</div>
</div>
{/* CENTER PANEL - Main Content */}
<div className="col-span-12 lg:col-span-6 space-y-4">
{/* Knowledge Base Viewer */}
{selectedKBArticle ? (
<KnowledgeBaseViewer
article={selectedKBArticle}
onClose={() => setSelectedKBArticle(null)}
/>
) : (
<>
{/* Quick Check */}
<div style={{...STYLES.intelCard, padding: '1.5rem'}} className="rounded-lg">
<div className="scan-line"></div>
@@ -1753,6 +1857,8 @@ export default function App() {
<p className="text-gray-300">Try adjusting your search criteria or filters</p>
</div>
)}
</>
)}
</div>
{/* End Center Panel */}

View File

@@ -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 (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()} style={{ maxWidth: '700px' }}>
{/* Header */}
<div className="modal-header">
<h2 className="modal-title">Knowledge Base</h2>
<button onClick={onClose} className="modal-close">
<X className="w-5 h-5" />
</button>
</div>
{/* Body */}
<div className="modal-body">
{/* Idle Phase - Upload Form */}
{phase === 'idle' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-2" style={{ color: '#94A3B8' }}>
Title *
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="e.g., Inventory Management Policy"
className="intel-input w-full"
maxLength={255}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: '#94A3B8' }}>
Description
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Brief description of this document..."
className="intel-input w-full"
rows={3}
maxLength={500}
/>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: '#94A3B8' }}>
Category
</label>
<select
value={category}
onChange={(e) => setCategory(e.target.value)}
className="intel-input w-full"
>
<option value="General">General</option>
<option value="Policy">Policy</option>
<option value="Procedure">Procedure</option>
<option value="Guide">Guide</option>
<option value="Reference">Reference</option>
</select>
</div>
<div>
<label className="block text-sm font-medium mb-2" style={{ color: '#94A3B8' }}>
Document File *
</label>
<input
type="file"
accept=".pdf,.md,.txt,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.html,.json,.yaml,.yml"
onChange={handleFileSelect}
className="intel-input w-full"
/>
{selectedFile && (
<p className="mt-2 text-sm" style={{ color: '#10B981' }}>
Selected: {selectedFile.name} ({formatFileSize(selectedFile.size)})
</p>
)}
</div>
<button
onClick={handleUpload}
disabled={!selectedFile || !title.trim()}
className={`intel-button w-full ${selectedFile && title.trim() ? 'intel-button-success' : 'opacity-50 cursor-not-allowed'}`}
>
<UploadIcon className="w-4 h-4 mr-2" />
Upload Document
</button>
{error && (
<div className="flex items-start gap-2 p-3 rounded" style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #EF4444' }}>
<AlertCircle className="w-5 h-5 flex-shrink-0" style={{ color: '#EF4444' }} />
<p style={{ color: '#FCA5A5' }}>{error}</p>
</div>
)}
</div>
)}
{/* Uploading Phase */}
{phase === 'uploading' && (
<div className="text-center py-8">
<Loader className="w-12 h-12 animate-spin mx-auto mb-4" style={{ color: '#0EA5E9' }} />
<p style={{ color: '#94A3B8' }}>Uploading document...</p>
</div>
)}
{/* Success Phase */}
{phase === 'success' && result && (
<div className="space-y-4">
<div className="flex items-center gap-2 p-4 rounded" style={{ background: 'rgba(16, 185, 129, 0.1)', border: '1px solid #10B981' }}>
<CheckCircle className="w-6 h-6" style={{ color: '#10B981' }} />
<div>
<p className="font-medium" style={{ color: '#34D399' }}>Upload Successful!</p>
<p className="text-sm mt-1" style={{ color: '#94A3B8' }}>
{result.title} has been added to the knowledge base.
</p>
</div>
</div>
<button onClick={resetForm} className="intel-button w-full">
Upload Another Document
</button>
</div>
)}
{/* Error Phase */}
{phase === 'error' && (
<div className="space-y-4">
<div className="flex items-start gap-2 p-4 rounded" style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #EF4444' }}>
<AlertCircle className="w-6 h-6 flex-shrink-0" style={{ color: '#EF4444' }} />
<div>
<p className="font-medium" style={{ color: '#FCA5A5' }}>Upload Failed</p>
<p className="text-sm mt-1" style={{ color: '#94A3B8' }}>{error}</p>
</div>
</div>
<button onClick={resetForm} className="intel-button w-full">
Try Again
</button>
</div>
)}
{/* Existing Articles Section */}
{(phase === 'idle' || phase === 'success') && existingArticles.length > 0 && (
<div className="mt-8">
<h3 className="text-lg font-medium mb-4" style={{ color: '#94A3B8' }}>
Existing Documents ({existingArticles.length})
</h3>
<div className="space-y-3 max-h-96 overflow-y-auto">
{existingArticles.map((article) => (
<div
key={article.id}
className="intel-card p-4"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<FileText className="w-4 h-4 flex-shrink-0" style={{ color: getCategoryColor(article.category) }} />
<p className="font-medium truncate" style={{ color: '#E2E8F0' }}>
{article.title}
</p>
</div>
{article.description && (
<p className="text-sm mb-2 line-clamp-2" style={{ color: '#94A3B8' }}>
{article.description}
</p>
)}
<div className="flex items-center gap-3 text-xs" style={{ color: '#64748B' }}>
<span
className="px-2 py-0.5 rounded"
style={{
background: `${getCategoryColor(article.category)}33`,
color: getCategoryColor(article.category)
}}
>
{article.category}
</span>
<span>{formatDate(article.created_at)}</span>
<span>{formatFileSize(article.file_size)}</span>
</div>
</div>
<div className="flex gap-2 flex-shrink-0">
<button
onClick={() => handleDownload(article.id, article.file_name)}
className="intel-button intel-button-small intel-button-success"
title="Download"
>
<Download className="w-3 h-3" />
</button>
<button
onClick={() => handleDelete(article.id, article.title)}
className="intel-button intel-button-small"
style={{ borderColor: '#EF4444', color: '#EF4444' }}
title="Delete"
>
<Trash2 className="w-3 h-3" />
</button>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,248 @@
import React, { useState, useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import { X, Download, Loader, AlertCircle, FileText, File } from 'lucide-react';
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
export default function KnowledgeBaseViewer({ article, onClose }) {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
fetchArticleContent();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [article.id]);
const fetchArticleContent = async () => {
setLoading(true);
setError('');
try {
const response = await fetch(`${API_BASE}/knowledge-base/${article.id}/content`, {
credentials: 'include'
});
if (!response.ok) throw new Error('Failed to fetch article content');
const text = await response.text();
setContent(text);
} catch (err) {
console.error('Error fetching article content:', err);
setError(err.message);
} finally {
setLoading(false);
}
};
const handleDownload = async () => {
try {
const response = await fetch(`${API_BASE}/knowledge-base/${article.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 = article.file_name;
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 isMarkdown = article.file_name?.endsWith('.md');
const isText = article.file_name?.endsWith('.txt');
const isPDF = article.file_name?.endsWith('.pdf');
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(article.file_name || '');
const getCategoryColor = (cat) => {
const colors = {
'General': '#94A3B8',
'Policy': '#0EA5E9',
'Procedure': '#10B981',
'Guide': '#F59E0B',
'Reference': '#8B5CF6'
};
return colors[cat] || '#94A3B8';
};
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
return (
<div
style={{
background: 'linear-gradient(135deg, rgba(30, 41, 59, 0.95) 0%, rgba(51, 65, 85, 0.9) 50%, rgba(30, 41, 59, 0.95) 100%)',
border: '2px solid rgba(14, 165, 233, 0.4)',
borderRadius: '0.5rem',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.6), 0 0 28px rgba(14, 165, 233, 0.15)',
padding: '1.5rem',
position: 'relative',
overflow: 'hidden'
}}
>
{/* Header */}
<div className="flex items-start justify-between mb-4 pb-4" style={{ borderBottom: '1px solid rgba(14, 165, 233, 0.2)' }}>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<FileText className="w-5 h-5" style={{ color: getCategoryColor(article.category) }} />
<h2 className="text-xl font-semibold" style={{ color: '#E2E8F0', fontFamily: 'monospace' }}>
{article.title}
</h2>
</div>
{article.description && (
<p className="text-sm mb-2" style={{ color: '#94A3B8' }}>
{article.description}
</p>
)}
<div className="flex items-center gap-3 text-xs" style={{ color: '#64748B' }}>
<span
className="px-2 py-1 rounded"
style={{
background: `${getCategoryColor(article.category)}33`,
color: getCategoryColor(article.category),
fontWeight: '600'
}}
>
{article.category}
</span>
<span>Created: {formatDate(article.created_at)}</span>
{article.created_by_username && (
<span>By: {article.created_by_username}</span>
)}
</div>
</div>
<div className="flex gap-2 ml-4">
<button
onClick={handleDownload}
className="intel-button intel-button-small"
title="Download"
>
<Download className="w-4 h-4" />
</button>
<button
onClick={onClose}
className="intel-button intel-button-small"
title="Close"
>
<X className="w-4 h-4" />
</button>
</div>
</div>
{/* Content */}
<div className="kb-content-area">
{loading && (
<div className="text-center py-12">
<Loader className="w-8 h-8 animate-spin mx-auto mb-4" style={{ color: '#0EA5E9' }} />
<p style={{ color: '#94A3B8' }}>Loading document...</p>
</div>
)}
{error && (
<div className="flex items-start gap-3 p-4 rounded" style={{ background: 'rgba(239, 68, 68, 0.1)', border: '1px solid #EF4444' }}>
<AlertCircle className="w-6 h-6 flex-shrink-0" style={{ color: '#EF4444' }} />
<div>
<p className="font-medium" style={{ color: '#FCA5A5' }}>Failed to Load Document</p>
<p className="text-sm mt-1" style={{ color: '#94A3B8' }}>{error}</p>
</div>
</div>
)}
{!loading && !error && (
<>
{/* Markdown Rendering */}
{isMarkdown && (
<div className="markdown-content">
<ReactMarkdown>{content}</ReactMarkdown>
</div>
)}
{/* Plain Text */}
{isText && !isMarkdown && (
<pre
className="text-sm p-4 rounded overflow-auto"
style={{
background: 'rgba(15, 23, 42, 0.8)',
border: '1px solid rgba(14, 165, 233, 0.2)',
color: '#E2E8F0',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
maxHeight: '600px'
}}
>
{content}
</pre>
)}
{/* PDF */}
{isPDF && (
<div className="w-full" style={{ height: '700px' }}>
<iframe
src={`${API_BASE}/knowledge-base/${article.id}/content`}
title={article.title}
className="w-full h-full rounded"
style={{
border: '1px solid rgba(14, 165, 233, 0.3)',
background: 'rgba(15, 23, 42, 0.8)'
}}
>
<div className="text-center py-12">
<File className="w-16 h-16 mx-auto mb-4" style={{ color: '#EF4444' }} />
<p className="mb-4" style={{ color: '#94A3B8' }}>
Your browser doesn't support PDF preview. Click the download button to view this file.
</p>
<button onClick={handleDownload} className="intel-button intel-button-success">
<Download className="w-4 h-4 mr-2" />
Download PDF
</button>
</div>
</iframe>
</div>
)}
{/* Images */}
{isImage && (
<div className="text-center">
<img
src={`${API_BASE}/knowledge-base/${article.id}/content`}
alt={article.title}
className="max-w-full h-auto rounded"
style={{ border: '1px solid rgba(14, 165, 233, 0.3)' }}
/>
</div>
)}
{/* Other file types */}
{!isMarkdown && !isText && !isPDF && !isImage && (
<div className="text-center py-12">
<File className="w-16 h-16 mx-auto mb-4" style={{ color: '#94A3B8' }} />
<p className="mb-4" style={{ color: '#94A3B8' }}>
Preview not available for this file type.
</p>
<button onClick={handleDownload} className="intel-button intel-button-success">
<Download className="w-4 h-4 mr-2" />
Download File
</button>
</div>
)}
</>
)}
</div>
</div>
);
}

155
ivantiAPI.py Normal file
View File

@@ -0,0 +1,155 @@
# Ivanti API class/wrapper | Evan Compton (P2886385), updated 11/13/2025
### ! README | IMPORTANT INFORMATION ! ###
# requires an "Ivanti_config.ini" file in the same directory
# edit "Ivanti_config_template.ini", then save as "Ivanti_config.ini"
### ? CODE PURPOSE ? ###
# the primary purpose of this class/wrapper is to export data as a Pandas Dataframe and/or a CSV file
# this class primarily targets these endpoints: host, tag, hostFinding, vulnerability
# it should work on other endpoints as well, but the 4 above are the only ones tested
# usage examples of this class are at the end of this file
# library imports
import requests, urllib3, configparser, pandas as pd
from requests.adapters import HTTPAdapter
from urllib3 import Retry
# fix (ignore) SSL verification...
# Charter-specific issue; feel free to fix this if you can...
from urllib3.exceptions import InsecureRequestWarning
urllib3.disable_warnings(InsecureRequestWarning)
# Ivanti API class
class Ivanti:
def __init__(self, config_file='./Ivanti_config.ini'):
# read our config file
config = configparser.ConfigParser()
config.read(config_file)
# set up environment & auth
PLATFORM = config.get('platform', 'url') + config.get('platform', 'api_ver')
IVANTI_API_KEY = config.get('secrets', 'api_key')
self.CLIENT_ID = config.get('platform', 'client_id')
self.URL_BASE = f'{PLATFORM}/client/{self.CLIENT_ID}'
# universal header for our requests
self.header = {
'x-api-key': IVANTI_API_KEY,
'content-type': 'application/json'
}
# dictionaries for filters and fields, sorted with keys by endpoint prefixes
self.filters = {}
self.fields = {}
return
# function used for HTTP requests- thank you, Ivanti... useful code
def request(max_retries=5, backoff_factor=0.5, status_forcelist=(419,429)):
"""
Create a Requests session that uses automatic retries.
:param max_retries: Maximum number of retries to attempt
:type max_retries: int
:param backoff_factor: Backoff factor used to calculate time between retries.
:type backoff_factor: float
:param status_forcelist: A tuple containing the response status codes that should trigger a retry.
:type status_forcelist: tuple
:return: Requests Session
:rtype: Requests Session Object
"""
session = requests.Session()
retry = Retry(
total=max_retries,
read=max_retries,
connect=max_retries,
backoff_factor=backoff_factor,
status_forcelist=status_forcelist,
)
adapter = HTTPAdapter(max_retries=retry)
session.mount('https://', adapter)
return session
# retrieve all filters for an endpoint (tag, host, etc)
def get_filters(self, endp='tag'):
URL_FILTERS = f'{self.URL_BASE}/{endp}/filter'
self.last_resp = self.request().get(URL_FILTERS, headers=self.header, verify=False)
self.filters[endp] = self.last_resp.json()
return self.filters[endp]
# retrieve all fields for an endpoint (tag, host, etc)
def get_fields(self, endp='tag'):
URL_FIELDS = f'{self.URL_BASE}/{endp}/export/template'
self.last_resp = self.request().get(URL_FIELDS, headers=self.header, verify=False)
self.fields[endp] = self.last_resp.json()['exportableFields']
return self.fields[endp]
# this uses the "{subject}/search" endpoint instead of "{subject}/export"
def search(self, endp='tag', save=None, pages=None, size=750):
'''
Uses the "/client/{client_id}/{subject}/search" endpoint to export data as JSON.
:param endp: String for endpoint name; host, tag, group, etc. (default: "tag")
:param save: String for filename to save, end with ".csv" (default: none)
:param pages: Integer to limit the number of pages to pull (default: all pages)
:param size: Integer defining how many records to pull per page (default: 750 records)
:return: Pandas DataFrame
'''
# most endpoints follow the same URL structure and usage pattern
# filters and fields dont matter for searches- only for exports!
URL_SEARCH = f'{self.URL_BASE}/{endp}/search'
body = {
'projection': 'basic', # can also be set to 'detail'
'sort': [
{
'field': 'id',
'direction': 'ASC'
}
],
'page': 0,
'size': size
}
# post a search, get first page
resp = self.request().post(URL_SEARCH, headers=self.header, json=body, verify=False)
if resp.status_code != 200:
raise Exception(f'[!] ERROR: Search failed.\n- code: {resp.status_code}\n- text: {resp.text}')
totalPages = int(resp.json()['page']['totalPages'])
totalRecords = int(resp.json()['page']['totalElements'])
body['page'] = int(resp.json()['page']['number']) + 1
msg = f'[?] Search requested for "{endp}"\n[?] Total pages: {totalPages}\n[?] Total records: {totalRecords}\n[?] Batch size: {size}'
if pages:
msg += f'\n[?] Page limit: {pages} pages'
print(msg)
# limit results?
if pages:
totalPages = pages
# loop until the last page
subject = f'{endp[:-1]}ies' if endp.endswith('y') else f'{endp}s'
data = []
while body['page'] < totalPages:
resp = self.request().post(URL_SEARCH, headers=self.header, json=body, verify=False)
body['page'] = int(resp.json()['page']['number']) + 1
data.extend(resp.json()['_embedded'][subject])
print(f'[?] Page progress: [{body["page"]}/{totalPages}] ({len(data)} total records retrieved)\r', end='')
print(f'\n[+] Search completed. {len(data)} records retrieved!')
# make a nice dataframe, save file if wanted, return the frame
df = pd.DataFrame(data)
if save:
df.to_csv(save, index=False)
return df
### ? EXAMPLE USAGE ? ###
# configure the connection and auth, create an instance object
#API = Ivanti('./Ivanti_config.ini')
# the "search" function goes to the "/client/{clientID}/{subject}/search" endpoint
#df = API.search('host', save='IvantiHostsTest_5pages.csv', pages=5)
#df = API.search('tag', save='IvantiTagsTest_5pages.csv', pages=5)
#df = API.search('hostFinding', save='IvantiHostFindingsTest_5pages.csv', pages=5)
#df = API.search('vulnerability', save='IvantiVulnerabilitiesTest_5pages.csv', pages=5)
# you can also retrieve all possible filters and exportable fields per subject
#filters = API.get_fields('host')
#fields = API.get_filters('tag')