359 lines
9.7 KiB
JavaScript
359 lines
9.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Comprehensive API Audit Script
|
|
* Cross-references api-docs.json against endpoints.js to identify missing endpoints
|
|
* and parameter discrepancies for complete MCP server coverage
|
|
*/
|
|
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
/**
|
|
* Load and parse API documentation
|
|
*/
|
|
function loadApiDocumentation() {
|
|
try {
|
|
const apiDocsPath = path.join(
|
|
process.cwd(),
|
|
"complete-api-parameters.json"
|
|
);
|
|
const apiDocsContent = fs.readFileSync(apiDocsPath, "utf8");
|
|
return JSON.parse(apiDocsContent);
|
|
} catch (error) {
|
|
console.error("❌ Error loading API documentation:", error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load and parse current endpoints configuration
|
|
*/
|
|
function loadCurrentEndpoints() {
|
|
try {
|
|
const endpointsPath = path.join(process.cwd(), "src/config/endpoints.js");
|
|
const endpointsContent = fs.readFileSync(endpointsPath, "utf8");
|
|
|
|
// Extract endpoint arrays using regex
|
|
const publicMatch = endpointsContent.match(
|
|
/export const PUBLIC_ENDPOINTS = \[([\s\S]*?)\];/
|
|
);
|
|
const providerMatch = endpointsContent.match(
|
|
/export const PROVIDER_ENDPOINTS = \[([\s\S]*?)\];/
|
|
);
|
|
const patientMatch = endpointsContent.match(
|
|
/export const PATIENT_ENDPOINTS = \[([\s\S]*?)\];/
|
|
);
|
|
const partnerMatch = endpointsContent.match(
|
|
/export const PARTNER_ENDPOINTS = \[([\s\S]*?)\];/
|
|
);
|
|
const affiliateMatch = endpointsContent.match(
|
|
/export const AFFILIATE_ENDPOINTS = \[([\s\S]*?)\];/
|
|
);
|
|
const networkMatch = endpointsContent.match(
|
|
/export const NETWORK_ENDPOINTS = \[([\s\S]*?)\];/
|
|
);
|
|
|
|
const endpoints = {
|
|
PUBLIC: [],
|
|
PROVIDER: [],
|
|
PATIENT: [],
|
|
PARTNER: [],
|
|
AFFILIATE: [],
|
|
NETWORK: [],
|
|
};
|
|
|
|
// Parse endpoints from each array
|
|
if (publicMatch) {
|
|
endpoints.PUBLIC = extractEndpointsFromText(publicMatch[1]);
|
|
}
|
|
if (providerMatch) {
|
|
endpoints.PROVIDER = extractEndpointsFromText(providerMatch[1]);
|
|
}
|
|
if (patientMatch) {
|
|
endpoints.PATIENT = extractEndpointsFromText(patientMatch[1]);
|
|
}
|
|
if (partnerMatch) {
|
|
endpoints.PARTNER = extractEndpointsFromText(partnerMatch[1]);
|
|
}
|
|
if (affiliateMatch) {
|
|
endpoints.AFFILIATE = extractEndpointsFromText(affiliateMatch[1]);
|
|
}
|
|
if (networkMatch) {
|
|
endpoints.NETWORK = extractEndpointsFromText(networkMatch[1]);
|
|
}
|
|
|
|
return endpoints;
|
|
} catch (error) {
|
|
console.error("❌ Error loading current endpoints:", error.message);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract endpoint objects from text using regex
|
|
*/
|
|
function extractEndpointsFromText(text) {
|
|
const endpoints = [];
|
|
const endpointRegex =
|
|
/\{[\s\S]*?path:\s*["']([^"']+)["'][\s\S]*?method:\s*["']([^"']+)["'][\s\S]*?\}/g;
|
|
|
|
let match;
|
|
while ((match = endpointRegex.exec(text)) !== null) {
|
|
endpoints.push({
|
|
path: match[1],
|
|
method: match[2].toUpperCase(),
|
|
});
|
|
}
|
|
|
|
return endpoints;
|
|
}
|
|
|
|
/**
|
|
* Categorize API endpoints by authentication type
|
|
*/
|
|
function categorizeApiEndpoints(apiEndpoints) {
|
|
const categorized = {
|
|
PUBLIC: [],
|
|
PROVIDER: [],
|
|
PATIENT: [],
|
|
PARTNER: [],
|
|
AFFILIATE: [],
|
|
NETWORK: [],
|
|
};
|
|
|
|
apiEndpoints.forEach((endpoint) => {
|
|
const path = endpoint.path;
|
|
const requiresAuth = endpoint.requiresAuth;
|
|
|
|
// Categorization logic based on path patterns and authentication
|
|
if (
|
|
!requiresAuth ||
|
|
path.includes("/login") ||
|
|
path.includes("/register") ||
|
|
path.includes("/password")
|
|
) {
|
|
categorized.PUBLIC.push(endpoint);
|
|
} else if (
|
|
path.includes("/emr/") ||
|
|
path.includes("/api/emr") ||
|
|
endpoint.tags?.some((tag) =>
|
|
["Provider", "Medical", "Clinical"].includes(tag)
|
|
)
|
|
) {
|
|
categorized.PROVIDER.push(endpoint);
|
|
} else if (
|
|
path.includes("/patient/") ||
|
|
path.includes("/frontend/") ||
|
|
endpoint.tags?.some((tag) => ["Patient", "Patient Portal"].includes(tag))
|
|
) {
|
|
categorized.PATIENT.push(endpoint);
|
|
} else if (
|
|
path.includes("/partner/") ||
|
|
endpoint.tags?.some((tag) => ["Partner"].includes(tag))
|
|
) {
|
|
categorized.PARTNER.push(endpoint);
|
|
} else if (
|
|
path.includes("/affiliate/") ||
|
|
endpoint.tags?.some((tag) => ["Affiliate"].includes(tag))
|
|
) {
|
|
categorized.AFFILIATE.push(endpoint);
|
|
} else if (
|
|
path.includes("/network/") ||
|
|
endpoint.tags?.some((tag) => ["Network"].includes(tag))
|
|
) {
|
|
categorized.NETWORK.push(endpoint);
|
|
} else {
|
|
// Default to PROVIDER for authenticated endpoints
|
|
categorized.PROVIDER.push(endpoint);
|
|
}
|
|
});
|
|
|
|
return categorized;
|
|
}
|
|
|
|
/**
|
|
* Find missing endpoints by comparing API docs with current implementation
|
|
*/
|
|
function findMissingEndpoints(apiEndpoints, currentEndpoints) {
|
|
const missing = {
|
|
PUBLIC: [],
|
|
PROVIDER: [],
|
|
PATIENT: [],
|
|
PARTNER: [],
|
|
AFFILIATE: [],
|
|
NETWORK: [],
|
|
};
|
|
|
|
Object.keys(apiEndpoints).forEach((authType) => {
|
|
const apiList = apiEndpoints[authType];
|
|
const currentList = currentEndpoints[authType] || [];
|
|
|
|
apiList.forEach((apiEndpoint) => {
|
|
const exists = currentList.some(
|
|
(current) =>
|
|
current.path === apiEndpoint.path &&
|
|
current.method === apiEndpoint.method.toUpperCase()
|
|
);
|
|
|
|
if (!exists) {
|
|
missing[authType].push(apiEndpoint);
|
|
}
|
|
});
|
|
});
|
|
|
|
return missing;
|
|
}
|
|
|
|
/**
|
|
* Generate audit report
|
|
*/
|
|
function generateAuditReport(apiEndpoints, currentEndpoints, missingEndpoints) {
|
|
const report = {
|
|
timestamp: new Date().toISOString(),
|
|
summary: {
|
|
totalApiEndpoints: 0,
|
|
totalCurrentEndpoints: 0,
|
|
totalMissingEndpoints: 0,
|
|
byAuthType: {},
|
|
},
|
|
missingEndpoints,
|
|
recommendations: [],
|
|
};
|
|
|
|
// Calculate totals
|
|
Object.keys(apiEndpoints).forEach((authType) => {
|
|
const apiCount = apiEndpoints[authType].length;
|
|
const currentCount = currentEndpoints[authType]?.length || 0;
|
|
const missingCount = missingEndpoints[authType].length;
|
|
|
|
report.summary.totalApiEndpoints += apiCount;
|
|
report.summary.totalCurrentEndpoints += currentCount;
|
|
report.summary.totalMissingEndpoints += missingCount;
|
|
|
|
report.summary.byAuthType[authType] = {
|
|
apiEndpoints: apiCount,
|
|
currentEndpoints: currentCount,
|
|
missingEndpoints: missingCount,
|
|
coverage:
|
|
currentCount > 0
|
|
? ((currentCount / apiCount) * 100).toFixed(1) + "%"
|
|
: "0%",
|
|
};
|
|
});
|
|
|
|
// Generate recommendations
|
|
Object.keys(missingEndpoints).forEach((authType) => {
|
|
if (missingEndpoints[authType].length > 0) {
|
|
report.recommendations.push({
|
|
authType,
|
|
action: `Implement ${missingEndpoints[authType].length} missing ${authType} endpoints`,
|
|
priority:
|
|
authType === "PROVIDER"
|
|
? "HIGH"
|
|
: authType === "PUBLIC"
|
|
? "MEDIUM"
|
|
: "LOW",
|
|
});
|
|
}
|
|
});
|
|
|
|
return report;
|
|
}
|
|
|
|
/**
|
|
* Main audit function
|
|
*/
|
|
function performAudit() {
|
|
console.log("🔍 Starting comprehensive API audit...\n");
|
|
|
|
// Load data
|
|
console.log("📋 Loading API documentation...");
|
|
const apiDocs = loadApiDocumentation();
|
|
console.log(`✅ Loaded ${apiDocs.length} API endpoints\n`);
|
|
|
|
console.log("📋 Loading current endpoint configuration...");
|
|
const currentEndpoints = loadCurrentEndpoints();
|
|
const currentTotal = Object.values(currentEndpoints).reduce(
|
|
(sum, arr) => sum + arr.length,
|
|
0
|
|
);
|
|
console.log(`✅ Loaded ${currentTotal} current endpoints\n`);
|
|
|
|
// Categorize API endpoints
|
|
console.log("🏷️ Categorizing API endpoints by authentication type...");
|
|
const categorizedApiEndpoints = categorizeApiEndpoints(apiDocs);
|
|
console.log("✅ Categorization complete\n");
|
|
|
|
// Find missing endpoints
|
|
console.log("🔍 Identifying missing endpoints...");
|
|
const missingEndpoints = findMissingEndpoints(
|
|
categorizedApiEndpoints,
|
|
currentEndpoints
|
|
);
|
|
console.log("✅ Analysis complete\n");
|
|
|
|
// Generate report
|
|
console.log("📊 Generating audit report...");
|
|
const report = generateAuditReport(
|
|
categorizedApiEndpoints,
|
|
currentEndpoints,
|
|
missingEndpoints
|
|
);
|
|
|
|
// Save report
|
|
const reportPath = path.join(
|
|
process.cwd(),
|
|
"comprehensive-api-audit-report.json"
|
|
);
|
|
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
console.log(`✅ Report saved to: ${reportPath}\n`);
|
|
|
|
// Display summary
|
|
console.log("📈 AUDIT SUMMARY:");
|
|
console.log(`Total API endpoints: ${report.summary.totalApiEndpoints}`);
|
|
console.log(
|
|
`Current implementation: ${report.summary.totalCurrentEndpoints}`
|
|
);
|
|
console.log(`Missing endpoints: ${report.summary.totalMissingEndpoints}`);
|
|
console.log(
|
|
`Overall coverage: ${(
|
|
(report.summary.totalCurrentEndpoints /
|
|
report.summary.totalApiEndpoints) *
|
|
100
|
|
).toFixed(1)}%\n`
|
|
);
|
|
|
|
// Display by auth type
|
|
console.log("📊 COVERAGE BY AUTHENTICATION TYPE:");
|
|
Object.keys(report.summary.byAuthType).forEach((authType) => {
|
|
const stats = report.summary.byAuthType[authType];
|
|
console.log(
|
|
`${authType}: ${stats.currentEndpoints}/${stats.apiEndpoints} (${stats.coverage}) - Missing: ${stats.missingEndpoints}`
|
|
);
|
|
});
|
|
|
|
console.log("\n🎯 RECOMMENDATIONS:");
|
|
report.recommendations.forEach((rec) => {
|
|
console.log(
|
|
`${
|
|
rec.priority === "HIGH" ? "🔴" : rec.priority === "MEDIUM" ? "🟡" : "🟢"
|
|
} ${rec.action} (${rec.priority} priority)`
|
|
);
|
|
});
|
|
|
|
return report;
|
|
}
|
|
|
|
// Run audit if called directly
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
performAudit();
|
|
}
|
|
|
|
// Also run if this is the main module (for Node.js compatibility)
|
|
if (process.argv[1] && process.argv[1].endsWith("comprehensive-api-audit.js")) {
|
|
performAudit();
|
|
}
|
|
|
|
export { performAudit };
|