diff --git a/docs/1secure/setup-and-configuration/_category_.json b/docs/1secure/setup-and-configuration/_category_.json new file mode 100644 index 0000000000..cf9c908a16 --- /dev/null +++ b/docs/1secure/setup-and-configuration/_category_.json @@ -0,0 +1,6 @@ +{ + "label": "Setup and Configuration", + "position": 35, + "collapsed": false, + "collapsible": true +} diff --git a/docs/kb/1secure/_category_.json b/docs/kb/1secure/_category_.json index bd0adf85a3..ecfd882251 100644 --- a/docs/kb/1secure/_category_.json +++ b/docs/kb/1secure/_category_.json @@ -1,5 +1,5 @@ { - "label": "Knowledge Base Articles", + "label": "Knowledge Base", "position": 999, "collapsed": true, "collapsible": true diff --git a/docs/kb/auditor/_category_.json b/docs/kb/auditor/_category_.json index 31ba0aed9c..ecfd882251 100644 --- a/docs/kb/auditor/_category_.json +++ b/docs/kb/auditor/_category_.json @@ -1,5 +1,5 @@ { - "label": "Troubleshooting Articles", + "label": "Knowledge Base", "position": 999, "collapsed": true, "collapsible": true diff --git a/docs/kb/changetracker/_category_.json b/docs/kb/changetracker/_category_.json index 8163414625..ecfd882251 100644 --- a/docs/kb/changetracker/_category_.json +++ b/docs/kb/changetracker/_category_.json @@ -1,5 +1,5 @@ { - "label": "Change Tracker Knowledge Base", + "label": "Knowledge Base", "position": 999, "collapsed": true, "collapsible": true diff --git a/docs/kb/directorymanager/_category_.json b/docs/kb/directorymanager/_category_.json index 209a45df71..d905920e1e 100644 --- a/docs/kb/directorymanager/_category_.json +++ b/docs/kb/directorymanager/_category_.json @@ -1,5 +1,5 @@ { - "label": "Troubleshooting Articles", + "label": "Knowledge Base", "position": 999, "collapsed": true, "collapsible": true diff --git a/docs/kb/endpointpolicymanager/_category_.json b/docs/kb/endpointpolicymanager/_category_.json index bd0adf85a3..ecfd882251 100644 --- a/docs/kb/endpointpolicymanager/_category_.json +++ b/docs/kb/endpointpolicymanager/_category_.json @@ -1,5 +1,5 @@ { - "label": "Knowledge Base Articles", + "label": "Knowledge Base", "position": 999, "collapsed": true, "collapsible": true diff --git a/docs/kb/threatprevention/agent-management-and-connectivity/scripts/README.md b/docs/kb/threatprevention/agent-management-and-connectivity/scripts/README.md index a15e7a04a8..57c5619d7c 100644 --- a/docs/kb/threatprevention/agent-management-and-connectivity/scripts/README.md +++ b/docs/kb/threatprevention/agent-management-and-connectivity/scripts/README.md @@ -1,13 +1,18 @@ # Threat Prevention Automation Scripts -The scripts in this folder are provided as **example reference implementations only**. +Automation scripts for Threat Prevention agent management are provided in the following Knowledge Base articles as **example reference implementations only**. -They are not supported tools and are not validated for use in customer -environments. Netwrix Threat Prevention Support does not customize or -troubleshoot these scripts. +## Important Disclaimer -You are responsible for reviewing, testing, and adapting any script -to meet your organization's security, certificate authority, -and change management requirements. +These scripts are not supported tools and are not validated for use in customer environments. Netwrix Threat Prevention Support does not customize or troubleshoot these scripts. -Refer to the associated Knowledge Base articles for full context and guidance. +You are responsible for reviewing, testing, and adapting any script to meet your organization's security, certificate authority, and change management requirements. + +**Please review the full KB article before using any script** to ensure you understand the context, prerequisites, and proper usage. + +## Available Scripts + +The following KB articles contain scripts and detailed instructions for agent deployment and management: + +- [Automating Custom Managed Cert Signing and Deployment](../automating-custom-managed-cert-signing-and-deployment) - Script for automating certificate signing and deployment in custom managed mode +- [Remote Deployment of Threat Prevention Agent in Custom Managed Mode](../remote-deployment-of-threat-prevention-agent-in-custom-managed-mode) - Script for remote agent deployment diff --git a/kb-categories-inventory.txt b/kb-categories-inventory.txt new file mode 100644 index 0000000000..9aac138a69 --- /dev/null +++ b/kb-categories-inventory.txt @@ -0,0 +1,192 @@ +==== 1secure ==== +0-images +permissions-and-access +troubleshooting + +==== accessanalyzer ==== +0-images +active-directory-auditing +connection-profiles-and-credentials +database-auditing-and-configuration +entra-id-and-azure-integration +exchange-online-integration +file-system-and-sensitive-data-discovery +installation-and-upgrades +job-management-and-scheduling +reference-and-technical-specifications +reports-and-web-console +sharepoint-online-integration +troubleshooting-and-errors + +==== accessinformationcenter ==== +configuration-and-setup +entitlement-reviews + +==== activitymonitor ==== +0-images +agent-configuration-and-management +best-practices-and-reference +integrations-and-data-sources +monitoring-platforms-and-storage +security-and-compliance +troubleshooting-and-errors + +==== auditor ==== +0-images +configuration-and-setup +configuration-and-setup/active-directory-auditing +configuration-and-setup/exchange-auditing +configuration-and-setup/file-server-auditing +configuration-and-setup/general-configuration +configuration-and-setup/microsoft-365-integration +configuration-and-setup/sharepoint-and-teams-auditing +configuration-and-setup/sql-server-auditing +configuration-and-setup/windows-server-monitoring +event-id-reference +event-id-reference/health-log-events +features-and-operations +features-and-operations/glossaries-and-faqs +monitoring-plans +monitoring-plans/account-lockout-examiner +monitoring-plans/event-log-management +monitoring-plans/password-expiration-notifier +monitoring-plans/user-activity-monitoring +reports-alerts-and-notifications +reports-alerts-and-notifications/alerts-and-rules +reports-alerts-and-notifications/email-notifications +reports-alerts-and-notifications/report-generation +reports-alerts-and-notifications/subscriptions-and-distribution +system-administration +system-administration/backup-and-recovery +system-administration/database-management +system-administration/licensing-and-compliance +system-administration/migration-and-upgrade +system-administration/security-hardening +troubleshooting-and-errors +troubleshooting-and-errors/connection-errors +troubleshooting-and-errors/data-collection-errors +troubleshooting-and-errors/permission-and-authentication-errors +troubleshooting-and-errors/ssl-tls-certificate-issues + +==== changetracker ==== +audit-and-logging +database-and-diagnostics +troubleshooting-and-errors + +==== dataclassification ==== +0-images +authentication-and-security +features-and-operations +migration-and-maintenance +system-administration +term-set-operations +troubleshooting-and-errors + +==== directorymanager ==== +0-images +configuration-and-integration +group-management-and-operations +portal-customization-and-ux +reporting-export-and-data-management +security-permissions-and-access-control +system-administration-and-maintenance +troubleshooting-and-errors +workflows-automation-and-lifecycle-management + +==== endpointpolicymanager ==== +images + +==== endpointprotector ==== +0-images +administration-security-and-monitoring +content-aware-protection-and-dpi +deployment-and-installation +device-control-and-access-management +enforced-encryption-and-easylock +features-and-operations +troubleshooting-and-errors + +==== general ==== +0-images +administration-and-maintenance +audit-and-logging +authentication-and-security +client-installation-and-deployment +cloud-and-permissions +configuration-and-customization +database-and-diagnostics +documentation-and-reference +infrastructure-and-network +security-and-compliance +setup-and-deployment +troubleshooting-and-errors + +==== passwordpolicyenforcer ==== +0-images +authentication-and-integration +email-and-mailer-configuration +installation-and-configuration +policy-rules-and-configuration +troubleshooting-and-errors + +==== passwordreset ==== +0-images +configuration-and-customization +deployment-and-infrastructure +enrollment-and-user-setup +integrations-and-notifications +portal-access-and-authentication +reporting-and-monitoring +security-and-administration + +==== privilegesecure ==== +0-images +active-directory-configuration +authentication-and-mfa +certificates-and-security +client-tools-and-integrations +database-and-backend +high-availability-and-clustering +installation-configuration-and-licensing +network-and-platform-integration +remote-desktop-and-rds +resource-management +session-management-and-recording +third-party-integrations +troubleshooting-and-errors +user-and-service-accounts +workflows-and-automation + +==== privilegesecurediscovery ==== +0-images +authentication-and-access +database-and-backend +linux-and-ssh-integration +security-and-compliance +system-configuration-and-requirements +troubleshooting-and-errors + +==== recoveryad ==== +0-images +configuration-and-administration +installation-and-deployment +operations-and-troubleshooting + +==== threatmanager ==== +0-images +configuration-and-administration +installation-and-upgrade +integration-and-event-collection +licensing-and-system-issues +threat-management-and-operations +troubleshooting-and-diagnostics + +==== threatprevention ==== +0-images +agent-installation-and-deployment +agent-management-and-connectivity +configuration-and-administration +database-and-maintenance +licensing +troubleshooting-and-diagnostics + diff --git a/kb-sidebars-to-migrate.txt b/kb-sidebars-to-migrate.txt new file mode 100644 index 0000000000..1531f2c9e7 --- /dev/null +++ b/kb-sidebars-to-migrate.txt @@ -0,0 +1,62 @@ +sidebars/dataclassification/5.6.2.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/dataclassification/5.6.2.js:13: items: generateKBSidebar('dataclassification') +sidebars/dataclassification/5.7.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/dataclassification/5.7.js:13: items: generateKBSidebar('dataclassification') +sidebars/directorymanager/11.1.js:1:// DIAGNOSTIC TEST: const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/directorymanager/11.1.js:14: // items: generateKBSidebar('directorymanager') +sidebars/directorymanager/11.0.js:1:// DIAGNOSTIC TEST: const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/directorymanager/11.0.js:14: // items: generateKBSidebar('directorymanager') +sidebars/endpointprotector/epp.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/endpointprotector/epp.js:13: items: generateKBSidebar('endpointprotector') +sidebars/activitymonitor/7.1.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/activitymonitor/7.1.js:13: items: generateKBSidebar('activitymonitor') +sidebars/activitymonitor/8.0.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/activitymonitor/8.0.js:13: items: generateKBSidebar('activitymonitor') +sidebars/activitymonitor/9.0.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/activitymonitor/9.0.js:13: items: generateKBSidebar('activitymonitor') +sidebars/threatprevention/7.5.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/threatprevention/7.5.js:13: items: generateKBSidebar('threatprevention') +sidebars/threatprevention/7.4.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/threatprevention/7.4.js:13: items: generateKBSidebar('threatprevention') +sidebars/threatprevention/8.0.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/threatprevention/8.0.js:13: items: generateKBSidebar('threatprevention') +sidebars/changetracker/8.0.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/changetracker/8.0.js:13: items: generateKBSidebar('changetracker') +sidebars/changetracker/8.1.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/changetracker/8.1.js:13: items: generateKBSidebar('changetracker') +sidebars/passwordpolicyenforcer/11.1.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/passwordpolicyenforcer/11.1.js:13: items: generateKBSidebar('passwordpolicyenforcer') +sidebars/passwordpolicyenforcer/11.0.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/passwordpolicyenforcer/11.0.js:13: items: generateKBSidebar('passwordpolicyenforcer') +sidebars/passwordpolicyenforcer/10.2.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/passwordpolicyenforcer/10.2.js:13: items: generateKBSidebar('passwordpolicyenforcer') +sidebars/privilegesecurediscovery.js:1:const generateKBSidebar = require('../src/utils/generateKBSidebar'); +sidebars/privilegesecurediscovery.js:13: items: generateKBSidebar('privilegesecurediscovery') +sidebars/threatmanager/3.0.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/threatmanager/3.0.js:13: items: generateKBSidebar('threatmanager') +sidebars/passwordreset/3.3.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/passwordreset/3.3.js:13: items: generateKBSidebar('passwordreset') +sidebars/passwordreset/3.23.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/passwordreset/3.23.js:13: items: generateKBSidebar('passwordreset') +sidebars/privilegesecure/25.12.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/privilegesecure/25.12.js:13: items: generateKBSidebar('privilegesecure') +sidebars/privilegesecure/4.2.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/privilegesecure/4.2.js:13: items: generateKBSidebar('privilegesecure') +sidebars/privilegesecure/4.1.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/privilegesecure/4.1.js:13: items: generateKBSidebar('privilegesecure') +sidebars/accessinformationcenter/12.0.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/accessinformationcenter/12.0.js:13: items: generateKBSidebar('accessinformationcenter') +sidebars/accessinformationcenter/11.6.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/accessinformationcenter/11.6.js:13: items: generateKBSidebar('accessinformationcenter') +sidebars/recoveryforactivedirectory/2.6.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/recoveryforactivedirectory/2.6.js:13: items: generateKBSidebar('recoveryad') +sidebars/recoveryforactivedirectory/3.1.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/recoveryforactivedirectory/3.1.js:13: items: generateKBSidebar('recoveryad') +sidebars/auditor/10.8.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/auditor/10.8.js:13: items: generateKBSidebar('auditor') +sidebars/auditor/10.6.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/auditor/10.6.js:13: items: generateKBSidebar('auditor') +sidebars/auditor/10.7.js:1:const generateKBSidebar = require('../../src/utils/generateKBSidebar'); +sidebars/auditor/10.7.js:13: items: generateKBSidebar('auditor') +src/utils/generateKBSidebar.js:135:function generateKBSidebar(productName) { +src/utils/generateKBSidebar.js:176:module.exports = generateKBSidebar; diff --git a/kb_allowlist.json b/kb_allowlist.json new file mode 100644 index 0000000000..ee374ab77b --- /dev/null +++ b/kb_allowlist.json @@ -0,0 +1,6 @@ +{ + "accessanalyzer": [ + "11.6", + "12.0" + ] +} diff --git a/package.json b/package.json index 6ff15109b4..ff6b1c7c82 100644 --- a/package.json +++ b/package.json @@ -5,20 +5,20 @@ "main": "docusaurus.config.js", "scripts": { "docusaurus": "npx docusaurus", - "prestart": "node scripts/copy-kb-to-versions.js", + "prestart": "node scripts/copy-kb-to-versions.mjs", "start": "cross-env NODE_OPTIONS=--max-old-space-size=16384 CHOKIDAR_USEPOLLING=false npx docusaurus start --port=4500 --no-open", "start-chok": "cross-env NODE_OPTIONS=--max-old-space-size=16384 CHOKIDAR_USEPOLLING=true npx docusaurus start --port=4500 --no-open", - "prebuild": "node scripts/copy-kb-to-versions.js", + "prebuild": "node scripts/copy-kb-to-versions.mjs", "build": "cross-env NODE_OPTIONS=--max-old-space-size=16384 npx docusaurus build", - "preci": "node scripts/copy-kb-to-versions.js", + "preci": "node scripts/copy-kb-to-versions.mjs", "ci": "npx docusaurus build", "swizzle": "npx docusaurus swizzle", "clear": "npx docusaurus clear", "serve": "npx serve -s build -l 8080", "write-translations": "npx docusaurus write-translations", "write-heading-ids": "npx docusaurus write-heading-ids", - "kb:clean": "node scripts/copy-kb-to-versions.js --clean", - "kb:dry": "node scripts/copy-kb-to-versions.js --dry", + "kb:clean": "node scripts/copy-kb-to-versions.mjs --clean", + "kb:dry": "node scripts/copy-kb-to-versions.mjs --dry", "kb:test:aa": "npm run kb:clean && npm run prestart && npm run start", "kb:prodtest:aa": "npm run kb:clean && npm run prebuild && npm run build && npm run serve" }, diff --git a/scripts/add-generated-index.mjs b/scripts/add-generated-index.mjs new file mode 100644 index 0000000000..7cd4728aae --- /dev/null +++ b/scripts/add-generated-index.mjs @@ -0,0 +1,132 @@ +#!/usr/bin/env node + +/** + * Add generated-index to all KB category folders + * This creates the icon grid view for category pages + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const KB_DIR = path.resolve(__dirname, '../docs/kb'); + +// Convert folder name to readable description +function folderToDescription(folderName, productName) { + // Convert kebab-case to Title Case + const title = folderName + .split('-') + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + + return `Knowledge base articles related to ${title.toLowerCase()} in ${productName}.`; +} + +// Convert folder name to product display name +function getProductDisplayName(productId) { + const names = { + '1secure': '1Secure', + 'accessanalyzer': 'Access Analyzer', + 'accessinformationcenter': 'Access Information Center', + 'activitymonitor': 'Activity Monitor', + 'auditor': 'Auditor', + 'changetracker': 'Change Tracker', + 'dataclassification': 'Data Classification', + 'directorymanager': 'Directory Manager', + 'endpointpolicymanager': 'Endpoint Policy Manager', + 'endpointprotector': 'Endpoint Protector', + 'passwordpolicyenforcer': 'Password Policy Enforcer', + 'passwordreset': 'Password Reset', + 'privilegesecure': 'Privilege Secure', + 'privilegesecurediscovery': 'Privilege Secure Discovery', + 'recoveryad': 'Identity Recovery', + 'threatmanager': 'Threat Manager', + 'threatprevention': 'Threat Prevention', + }; + return names[productId] || productId; +} + +function updateCategoryFile(filePath, productName, categoryName) { + try { + const content = fs.readFileSync(filePath, 'utf8'); + const category = JSON.parse(content); + + // Check if already has generated-index + if (category.link && category.link.type === 'generated-index') { + console.log(` โœ“ Already has generated-index: ${categoryName}`); + return false; + } + + // Add or update the link field + category.link = { + type: 'generated-index', + description: folderToDescription(categoryName, productName) + }; + + // Write back + fs.writeFileSync(filePath, JSON.stringify(category, null, 2) + '\n', 'utf8'); + console.log(` โœ“ Updated: ${categoryName}`); + return true; + + } catch (error) { + console.error(` โœ— Error updating ${filePath}:`, error.message); + return false; + } +} + +function processProduct(productDir) { + const productName = path.basename(productDir); + const displayName = getProductDisplayName(productName); + + console.log(`\n๐Ÿ“š ${displayName} (${productName})`); + console.log('โ”€'.repeat(60)); + + let updated = 0; + + // Find all subdirectories (exclude image folders) + const entries = fs.readdirSync(productDir, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + + // Skip image/asset folders + if (entry.name.startsWith('0-') || ['images', 'assets', 'media'].includes(entry.name)) { + continue; + } + + const categoryPath = path.join(productDir, entry.name); + const categoryFile = path.join(categoryPath, '_category_.json'); + + if (fs.existsSync(categoryFile)) { + if (updateCategoryFile(categoryFile, displayName, entry.name)) { + updated++; + } + } + } + + return updated; +} + +function main() { + console.log('Adding generated-index to all KB category folders...\n'); + + let totalUpdated = 0; + + // Process each product + const products = fs.readdirSync(KB_DIR, { withFileTypes: true }); + + for (const product of products) { + if (!product.isDirectory()) continue; + + const productPath = path.join(KB_DIR, product.name); + totalUpdated += processProduct(productPath); + } + + console.log('\n' + '='.repeat(60)); + console.log(`โœ… Complete! Updated ${totalUpdated} category files`); + console.log('='.repeat(60)); +} + +main(); diff --git a/scripts/copy-kb-to-versions.js b/scripts/copy-kb-to-versions.mjs similarity index 59% rename from scripts/copy-kb-to-versions.js rename to scripts/copy-kb-to-versions.mjs index a2614a100a..9f80ca14c6 100644 --- a/scripts/copy-kb-to-versions.js +++ b/scripts/copy-kb-to-versions.mjs @@ -23,8 +23,13 @@ * COPY_KB_VERSIONS=12.0,11.6 # Filter by versions */ -const fs = require('fs'); -const path = require('path'); +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import { PRODUCTS } from '../src/config/products.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); // ============================================================================ // Global Constants @@ -32,29 +37,73 @@ const path = require('path'); const PROJECT_ROOT = path.resolve(__dirname, '..'); const LOCKFILE = path.join(PROJECT_ROOT, '.kb-copy.lock'); +const ASSET_DIRS = ['0-images', 'images', 'assets', 'media']; -const CONFIG = { - accessanalyzer: { - versions: ['12.0', '11.6'], - source: 'docs/kb/accessanalyzer', - destinationPattern: 'docs/accessanalyzer/{version}/kb' - } -}; - -const CATEGORY_LABELS = { - 'active-directory-auditing': 'Active Directory Auditing', - 'connection-profiles-and-credentials': 'Connection Profiles and Credentials', - 'database-auditing-and-configuration': 'Database Auditing and Configuration', - 'entra-id-and-azure-integration': 'Entra ID and Azure Integration', - 'exchange-online-integration': 'Exchange Online Integration', - 'file-system-and-sensitive-data-discovery': 'File System and Sensitive Data Discovery', - 'installation-and-upgrades': 'Installation and Upgrades', - 'job-management-and-scheduling': 'Job Management and Scheduling', - 'reference-and-technical-specifications': 'Reference and Technical Specifications', - 'reports-and-web-console': 'Reports and Web Console', - 'sharepoint-online-integration': 'SharePoint Online Integration', - 'troubleshooting-and-errors': 'Troubleshooting and Errors' -}; +// ============================================================================ +// Product Migration Control +// ============================================================================ + +/** + * Products that have been migrated to use the new autogenerated KB sidebar. + * + * For products in this list: + * - KB content will be copied to versioned docs folders + * - Sidebars use autogenerated structure (no generateKBSidebar() call) + * + * For products NOT in this list: + * - KB content will NOT be copied (script skips them) + * - Sidebars continue using generateKBSidebar() (old method) + * + * Add products to this list as their Phase 2 migration PR is merged. + */ +const MIGRATED_PRODUCTS = [ + // Add product IDs here as they complete migration + // Example: 'auditor', 'privilegesecure', 'threatprevention' + 'accessanalyzer', // Already migrated - uses autogenerated sidebar +]; + +// Build CONFIG dynamically from PRODUCTS +function buildConfig() { + const config = {}; + + PRODUCTS.forEach(product => { + // Validate required fields + if (!product.id || typeof product.id !== 'string') { + throw new Error(`Product missing required field 'id' or id is not a string: ${JSON.stringify(product)}`); + } + if (!Array.isArray(product.versions)) { + throw new Error(`Product '${product.id}' missing required array field 'versions'`); + } + + const productId = product.id; + const versions = product.versions.map(v => { + if (!v.version || typeof v.version !== 'string') { + throw new Error(`Product '${productId}' has version entry missing 'version' field: ${JSON.stringify(v)}`); + } + return v.version; + }); + + // Special handling: KB folder name mapping (for legacy naming) + const kbFolderName = productId === 'recoveryforactivedirectory' ? 'recoveryad' : productId; + + // Special handling: Unversioned products (version: 'current') + // For these products, copy KB to root level instead of /current/ folder + const hasCurrentVersion = versions.includes('current'); + const destinationPattern = hasCurrentVersion + ? `docs/${productId}/kb` + : `docs/${productId}/{version}/kb`; + + config[productId] = { + versions: versions, + source: `docs/kb/${kbFolderName}`, + destinationPattern: destinationPattern + }; + }); + + return config; +} + +const CONFIG = buildConfig(); // ============================================================================ // Lockfile Management @@ -140,9 +189,15 @@ function releaseLock(isDryRun) { // ============================================================================ function validateVersionFormat(version) { - const versionRegex = /^\d+\.\d+(\.\d+)?$/; // X.Y or X.Y.Z - if (!versionRegex.test(version)) { - throw new Error(`Invalid version format: ${version}. Expected X.Y or X.Y.Z`); + // Allow common version formats: X.Y, X.Y.Z, "current", "saas", etc. + // Just ensure it's a non-empty string with valid path characters + if (!version || typeof version !== 'string' || version.trim() === '') { + throw new Error(`Invalid version: empty or not a string`); + } + + // Check for path traversal attempts + if (version.includes('..') || version.includes('/') || version.includes('\\')) { + throw new Error(`Invalid version format: ${version}. Contains path traversal characters`); } } @@ -284,27 +339,104 @@ function rewriteAndCopyMarkdownFile(srcPath, destPath, kbSourceRoot, productName } // ============================================================================ -// Category File Generation (Whitelist) +// Category File Generation (Title Case with Acronym Support) // ============================================================================ +// Memoization cache for markdown file detection +const markdownCache = new Map(); + +function hasMarkdownFiles(dirPath) { + if (markdownCache.has(dirPath)) { + return markdownCache.get(dirPath); + } + + let result = false; + + try { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdx'))) { + result = true; + break; + } + + // Recursively check subdirectories + if (entry.isDirectory() && !entry.name.startsWith('.')) { + const subPath = path.join(dirPath, entry.name); + if (hasMarkdownFiles(subPath)) { + result = true; + break; + } + } + } + } catch (error) { + result = false; + } + + markdownCache.set(dirPath, result); + return result; +} + +function toTitleCase(str) { + const acronyms = ['AD', 'API', 'ID', 'SQL', 'SSL', 'TLS', 'MFA', 'SSO', 'RDS', 'UX', 'DPI']; + + return str + .split('-') + .map(word => { + const upper = word.toUpperCase(); + if (acronyms.includes(upper)) { + return upper; + } + return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(); + }) + .join(' '); +} + function generateCategoryFile(destPath, folderName) { - const label = CATEGORY_LABELS[folderName]; + // Skip hidden and underscore directories + if (folderName.startsWith('.') || folderName.startsWith('_')) { + return null; + } - if (!label) { - return null; // Not in whitelist + // Skip asset directories + if (ASSET_DIRS.includes(folderName.toLowerCase())) { + return null; } + const categoryPath = path.join(destPath, '_category_.json'); + + // Only generate if directory contains markdown files (recursive check) + if (!hasMarkdownFiles(destPath)) { + return null; + } + + const label = toTitleCase(folderName); + try { const categoryConfig = { label: label, collapsed: true, - collapsible: true + collapsible: true, + link: { + type: 'generated-index', + description: `Knowledge base articles related to ${label.toLowerCase()}.` + } }; - const categoryFilePath = path.join(destPath, '_category_.json'); - fs.writeFileSync(categoryFilePath, JSON.stringify(categoryConfig, null, 2) + '\n', 'utf8'); + // Use 'wx' flag to write only if file doesn't exist (atomic operation) + // This prevents race conditions between checking and writing + fs.writeFileSync(categoryPath, JSON.stringify(categoryConfig, null, 2) + '\n', { + encoding: 'utf8', + flag: 'wx' + }); return true; // Created successfully } catch (err) { + // File already exists (EEXIST) - skip without warning + if (err.code === 'EEXIST') { + return null; + } + // Other errors - log and report failure console.log(` โš ๏ธ Failed to generate category file for ${folderName}: ${err.message}`); return false; // Failed } @@ -340,19 +472,24 @@ function copyDirectorySync(src, dest, kbSourceRoot, productName, errorCount) { const destPath = path.join(dest, entry.name); if (entry.isDirectory()) { + // Skip hidden directories only (not underscore) + if (entry.name.startsWith('.')) { + continue; + } + // Recursively copy subdirectory (fatal errors throw up to per-version handler) const subCounts = copyDirectorySync(srcPath, destPath, kbSourceRoot, productName, errorCount); filesCount += subCounts.filesCount; categoriesCount += subCounts.categoriesCount; - // Generate category file (always call, whitelist inside) + // Generate category file (rules applied inside function) const categoryResult = generateCategoryFile(destPath, entry.name); if (categoryResult === true) { categoriesCount++; } else if (categoryResult === false) { errorCount.count++; } - // null = not whitelisted, ignore + // null = skipped, ignore } else { // Copy files (non-fatal failures, log and continue) if (entry.name.endsWith('.md')) { @@ -429,15 +566,24 @@ function main() { // Acquire lock acquireLock(isDryRun); - // Read environment filters - const filterProducts = process.env.COPY_KB_PRODUCTS + // Read environment filters (CLI flags override env vars) + let filterProducts = process.env.COPY_KB_PRODUCTS ? process.env.COPY_KB_PRODUCTS.split(',').map(p => p.trim()).filter(Boolean) : null; - const filterVersions = process.env.COPY_KB_VERSIONS + let filterVersions = process.env.COPY_KB_VERSIONS ? process.env.COPY_KB_VERSIONS.split(',').map(v => v.trim()).filter(Boolean) : null; + // CLI flags override environment variables + args.forEach(arg => { + if (arg.startsWith('--products=')) { + filterProducts = arg.split('=')[1].split(',').map(p => p.trim()).filter(Boolean); + } else if (arg.startsWith('--versions=')) { + filterVersions = arg.split('=')[1].split(',').map(v => v.trim()).filter(Boolean); + } + }); + // Validate environment validateEnvironment(filterProducts, filterVersions, CONFIG); validateConfig(CONFIG); @@ -451,7 +597,7 @@ function main() { } if (isClean) { - console.log('๐Ÿงน CLEAN MODE - Removing copied KB folders'); + console.log('๐Ÿงน CLEAN MODE - Will remove destinations before copy'); } if (filterProducts) { @@ -471,6 +617,14 @@ function main() { continue; } + // Skip products that haven't been migrated yet + // If MIGRATED_PRODUCTS is empty, skip all products (nothing migrated yet) + // If MIGRATED_PRODUCTS has items, only process products in the list + if (MIGRATED_PRODUCTS.length === 0 || !MIGRATED_PRODUCTS.includes(product)) { + console.log(`\nโญ๏ธ Skipping ${product} (not yet migrated)`); + continue; + } + console.log(`\n๐Ÿ“š Product: ${product}`); console.log('-'.repeat(60)); @@ -501,59 +655,36 @@ function main() { console.log(` Source: ${config.source}`); console.log(` Dest: ${destination}`); - if (isClean) { - // Remove copied KB folder - if (fs.existsSync(destination)) { - if (!isDryRun) { - const success = removeDirectorySync(destination); - if (success) { - console.log(` โœ… Removed`); - totalSuccess++; - } else { - totalVersionErrors++; - } + // If --clean is set, remove destination first + if (isClean && fs.existsSync(destination)) { + if (!isDryRun) { + const removeSuccess = removeDirectorySync(destination); + if (removeSuccess) { + console.log(` ๐Ÿ—‘๏ธ Removed old KB folder`); } else { - console.log(` ๐Ÿ” Would remove`); - totalSuccess++; + throw new Error('Failed to remove old KB folder'); } } else { - console.log(` โ„น๏ธ Does not exist (nothing to remove)`); - totalSuccess++; - } - } else { - // Copy KB content - - // Remove existing destination first - if (fs.existsSync(destination)) { - if (!isDryRun) { - const removeSuccess = removeDirectorySync(destination); - if (removeSuccess) { - console.log(` ๐Ÿ—‘๏ธ Removed old KB folder`); - } else { - throw new Error('Failed to remove old KB folder'); - } - } else { - console.log(` ๐Ÿ” Would remove old KB folder`); - } + console.log(` ๐Ÿ” [dry-run] Would remove old KB folder`); } + } - // Copy source to destination - if (!isDryRun) { - const errorCount = { count: 0 }; - const result = copyDirectorySync(config.source, destination, config.source, product, errorCount); + // Copy KB content (always happens, regardless of --clean) + if (!isDryRun) { + const errorCount = { count: 0 }; + const result = copyDirectorySync(config.source, destination, config.source, product, errorCount); - if (errorCount.count > 0) { - console.log(` โš ๏ธ Copied with ${errorCount.count} file errors`); - totalFileErrors += errorCount.count; - totalSuccess++; // Version partially succeeded - } else { - console.log(` โœ… Copied ${result.filesCount} files, ${result.categoriesCount} categories`); - totalSuccess++; - } + if (errorCount.count > 0) { + console.log(` โš ๏ธ Copied with ${errorCount.count} file errors`); + totalFileErrors += errorCount.count; + totalSuccess++; // Version partially succeeded } else { - console.log(` ๐Ÿ” Would copy KB content`); + console.log(` โœ… Copied ${result.filesCount} files, ${result.categoriesCount} categories`); totalSuccess++; } + } else { + console.log(` ๐Ÿ” [dry-run] Would copy KB content`); + // Don't count dry-run as success } } catch (err) { // Per-version error isolation @@ -564,9 +695,64 @@ function main() { } } + // Generate allowlist + const allowlist = {}; + const productsToProcess = filterProducts || Object.keys(CONFIG); + + for (const productKey of productsToProcess) { + const productConfig = CONFIG[productKey]; + if (!productConfig) continue; + + const versionsToProcess = filterVersions + ? filterVersions.filter(v => productConfig.versions.includes(v)) + : productConfig.versions; + + for (const version of versionsToProcess) { + const destination = productConfig.destinationPattern.replace('{version}', version); + const kbDestination = path.resolve(PROJECT_ROOT, destination); + + // Only include if destination exists AND contains markdown files + if (fs.existsSync(kbDestination) && hasMarkdownFiles(kbDestination)) { + if (!allowlist[productKey]) { + allowlist[productKey] = []; + } + if (!allowlist[productKey].includes(version)) { + allowlist[productKey].push(version); + } + } + } + } + + // Sort products and versions for deterministic output + const sortedAllowlist = {}; + Object.keys(allowlist).sort().forEach(key => { + sortedAllowlist[key] = allowlist[key].sort(); + }); + + const allowlistPath = path.join(PROJECT_ROOT, 'kb_allowlist.json'); + const allowlistContent = JSON.stringify(sortedAllowlist, null, 2) + '\n'; + + if (isDryRun) { + console.log('\n' + '='.repeat(60)); + console.log('๐Ÿ” [dry-run] Would generate kb_allowlist.json'); + console.log('Note: In dry-run mode, allowlist is computed from current filesystem state.'); + console.log('='.repeat(60)); + console.log(allowlistContent); + } else { + fs.writeFileSync(allowlistPath, allowlistContent, 'utf8'); + console.log('\n' + '='.repeat(60)); + console.log(`โœ… Generated: ${allowlistPath}`); + console.log('='.repeat(60)); + } + console.log('\n' + '='.repeat(60)); - console.log(isClean ? '๐Ÿงน Clean complete' : 'โœ… Copy complete'); - console.log(`Total: ${totalSuccess} successful, ${totalFileErrors} file errors, ${totalVersionErrors} version errors`); + if (isDryRun) { + console.log('๐Ÿ” Dry-run complete'); + console.log('Note: No files were modified. Re-run without --dry to apply changes.'); + } else { + console.log('โœ… Copy complete'); + console.log(`Total: ${totalSuccess} successful, ${totalFileErrors} file errors, ${totalVersionErrors} version errors`); + } console.log('='.repeat(60)); } catch (err) { @@ -578,8 +764,12 @@ function main() { } // Single exit point - const totalErrors = totalFileErrors + totalVersionErrors; - process.exit(totalErrors > 0 ? 1 : 0); + // Exit with success if ANY products copied successfully + // Only fail if there were no successes at all + if (totalSuccess === 0 && (totalFileErrors > 0 || totalVersionErrors > 0)) { + process.exit(1); + } + process.exit(0); } // Run