`;
// Handle different node types with collapsible sections
if (node.type === 'correlation_object') {
detailsHtml += this.generateCorrelationDetails(node);
} else if (node.type === 'large_entity') {
detailsHtml += this.generateLargeEntityDetails(node);
} else {
detailsHtml += this.generateStandardNodeDetails(node);
}
detailsHtml += '
';
return detailsHtml;
}
generateStandardNodeDetails(node) {
let html = '';
// Relationships sections
html += this.generateRelationshipsSection(node);
// Enhanced attributes section with special certificate handling
if (node.attributes && Object.keys(node.attributes).length > 0) {
const { certificates, ...otherAttributes } = node.attributes;
// Handle certificates separately with enhanced display
if (certificates) {
html += this.generateCertificateSection({ certificates });
}
// Handle other attributes normally
if (Object.keys(otherAttributes).length > 0) {
html += this.generateAttributesSection(otherAttributes);
}
}
// Description section
html += this.generateDescriptionSection(node);
// Metadata section (collapsed by default)
html += this.generateMetadataSection(node);
return html;
}
/**
* Enhanced certificate section generation using existing styles
*/
generateCertificateSection(attributes) {
const certificates = attributes.certificates;
if (!certificates || typeof certificates !== 'object') {
return '';
}
let html = `
đ SSL/TLS Certificates
`;
// Certificate summary using existing grid pattern
html += this.generateCertificateSummary(certificates);
// Latest certificate info using existing attribute display
if (certificates.latest_certificate) {
html += this.generateLatestCertificateInfo(certificates.latest_certificate);
}
// Detailed certificate list if available
if (certificates.certificate_details && Array.isArray(certificates.certificate_details)) {
html += this.generateCertificateList(certificates.certificate_details);
}
html += '
';
return html;
}
/**
* Generate latest certificate info using existing attribute list
*/
generateLatestCertificateInfo(latest) {
const isValid = latest.is_currently_valid;
const statusText = isValid ? 'Valid' : 'Invalid/Expired';
const statusColor = isValid ? '#00ff41' : '#ff6b6b';
let html = `
';
return html;
}
generateStandardNodeLayout(node) {
let html = '
';
// Relationships section
html += this.generateRelationshipsSection(node);
// Attributes section with smart categorization
html += this.generateAttributesSection(node);
// Description section
html += this.generateDescriptionSection(node);
// Metadata section (collapsed by default)
html += this.generateMetadataSection(node);
html += '
';
return html;
}
generateRelationshipsSection(node) {
let html = '';
if (node.incoming_edges && node.incoming_edges.length > 0) {
html += `
${key}: `;
if (typeof value === 'object' && value !== null) {
if (Array.isArray(value)) {
html += `[${value.length} items]`;
} else {
html += `{${Object.keys(value).length} properties}`;
}
} else {
html += this.escapeHtml(String(value));
}
html += '
`;
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Recursively formats a JavaScript object into an HTML unordered list with collapsible sections.
* @param {Object} obj - The object to format.
* @returns {string} - An HTML string representing the object.
*/
formatObjectToHtml(obj) {
if (!obj || Object.keys(obj).length === 0) {
return '
No data available.
';
}
let html = '
';
for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
const value = obj[key];
const formattedKey = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
if (typeof value === 'object' && value !== null) {
html += `
${formattedKey}`;
html += this.formatObjectToHtml(value);
html += `
`;
} else {
html += `
${formattedKey}: ${this.formatValue(value)}
`;
}
}
}
html += '
';
return html;
}
/**
* Enhanced showNodeModal with better event handling
*/
showNodeModal(node) {
if (!this.elements.nodeModal || !node) return;
if (this.elements.modalTitle) {
this.elements.modalTitle.innerHTML = `
${this.getNodeTypeIcon(node.type)}${node.id}
`;
}
const detailsHtml = this.generateNodeDetailsHtml(node);
if (this.elements.modalDetails) {
this.elements.modalDetails.innerHTML = detailsHtml;
// Add enhanced event handlers
this.addModalEventHandlers();
}
this.elements.nodeModal.style.display = 'block';
}
/**
* Add event handlers for enhanced modal interactions
*/
addModalEventHandlers() {
// Handle node navigation links - FIXED to work properly
this.elements.modalDetails.querySelectorAll('.node-link-compact').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const nodeId = e.target.dataset.nodeId || e.target.getAttribute('data-node-id');
console.log('Node link clicked:', nodeId);
if (nodeId && this.graphManager && this.graphManager.nodes) {
const nextNode = this.graphManager.nodes.get(nodeId);
if (nextNode) {
console.log('Navigating to node:', nextNode);
// Don't hide modal, just update content
this.showNodeModal(nextNode);
} else {
console.warn('Node not found:', nodeId);
}
}
});
});
// Handle the new extract button
this.elements.modalDetails.querySelectorAll('.extract-node-btn').forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const largeEntityId = e.target.dataset.largeEntityId;
const nodeId = e.target.dataset.nodeId;
console.log(`Extract button clicked for node ${nodeId} from entity ${largeEntityId}`);
this.extractNode(largeEntityId, nodeId);
});
});
// Handle legacy node links
this.elements.modalDetails.querySelectorAll('.node-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const nodeId = e.target.dataset.nodeId || e.target.getAttribute('data-node-id');
if (nodeId && this.graphManager && this.graphManager.nodes) {
const nextNode = this.graphManager.nodes.get(nodeId);
if (nextNode) {
this.showNodeModal(nextNode);
}
}
});
});
}
async extractNode(largeEntityId, nodeId) {
try {
this.showInfo(`Extraction initiated for ${nodeId}. It will be processed by the scanner.`);
const response = await this.apiCall('/api/graph/large-entity/extract', 'POST', {
large_entity_id: largeEntityId,
node_id: nodeId,
});
if (response.success) {
this.showSuccess(response.message);
// The node is now in the queue. We don't need to force a graph update.
// Instead, we just need to update the modal view to show one less item.
const graphResponse = await this.apiCall('/api/graph');
if (graphResponse.success) {
const updatedLargeEntity = graphResponse.graph.nodes.find(n => n.id === largeEntityId);
if (updatedLargeEntity) {
this.showNodeModal(updatedLargeEntity);
} else {
// The entity might have been dismantled completely if it was the last node
this.hideModal();
}
}
// If the scanner was idle, it's now running. Start polling.
if (this.scanStatus === 'idle') {
this.startPolling(1000);
}
} else {
throw new Error(response.error || 'Extraction failed on the server.');
}
} catch (error) {
console.error('Failed to extract node:', error);
this.showError(`Extraction failed: ${error.message}`);
}
}
initializeModalFunctionality() {
// Make sure the graph manager has node access
console.log('Initializing modal functionality...');
// Set up event delegation for dynamic content
document.addEventListener('click', (e) => {
const target = e.target.closest('.node-link-compact, .node-link');
if (target) {
e.preventDefault();
e.stopPropagation();
const nodeId = target.dataset.nodeId || target.getAttribute('data-node-id');
if (nodeId && this.graphManager && this.graphManager.nodes) {
const nextNode = this.graphManager.nodes.get(nodeId);
if (nextNode) {
this.showNodeModal(nextNode);
}
}
}
});
}
/**
* Get icon for node type
*/
getNodeTypeIcon(nodeType) {
const icons = {
'domain': 'đ',
'ip': 'đ',
'asn': 'đĸ',
'large_entity': 'đĻ',
'correlation_object': 'đ'
};
return icons[nodeType] || 'â';
}
/**
* Enhanced hideModal with animation
*/
hideModal() {
if (this.elements.nodeModal) {
this.elements.nodeModal.classList.add('modal-closing');
setTimeout(() => {
this.elements.nodeModal.style.display = 'none';
this.elements.nodeModal.classList.remove('modal-closing');
}, 200);
}
}
/**
* Copy text to clipboard with user feedback
*/
async copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
this.showMessage('Copied to clipboard', 'success');
} catch (err) {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.opacity = '0';
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
this.showMessage('Copied to clipboard', 'success');
} catch (fallbackErr) {
this.showMessage('Failed to copy text', 'error');
}
document.body.removeChild(textArea);
}
}
/**
* Toggle all entity nodes in large entity view
*/
toggleAllEntities() {
const entityCards = this.elements.modalDetails.querySelectorAll('.entity-node-card');
const allExpanded = Array.from(entityCards).every(card => card.classList.contains('expanded'));
entityCards.forEach(card => {
if (allExpanded) {
card.classList.remove('expanded');
} else {
card.classList.add('expanded');
}
});
// Update button text
const toggleBtn = this.elements.modalDetails.querySelector('.toggle-all-btn');
if (toggleBtn) {
toggleBtn.textContent = allExpanded ? 'Expand All' : 'Collapse All';
}
}
/**
* Enhanced keyboard navigation for modals
*/
setupModalKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
if (this.elements.nodeModal && this.elements.nodeModal.style.display === 'block') {
switch (e.key) {
case 'Escape':
this.hideModal();
break;
case 'Tab':
this.handleModalTabNavigation(e);
break;
case 'Enter':
if (e.target.classList.contains('node-link') || e.target.classList.contains('node-link-item')) {
e.target.click();
}
break;
}
}
});
}
/**
* Handle tab navigation within modal
*/
handleModalTabNavigation(e) {
const focusableElements = this.elements.nodeModal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
/**
* Initialize enhanced modal functionality
*/
initializeEnhancedModals() {
this.setupModalKeyboardNavigation();
// Add CSS classes for animations
const style = document.createElement('style');
style.textContent = `
.modal-opening {
animation: modalFadeIn 0.3s ease-out;
}
.modal-closing {
animation: modalFadeOut 0.2s ease-in;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes modalFadeOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
.array-value.expanded .array-items {
max-height: none;
}
.modal-title-icon {
margin-right: 0.5rem;
font-size: 1.2rem;
}
.modal-title-text {
font-family: 'Special Elite', monospace;
}
`;
document.head.appendChild(style);
}
/**
* Show Settings modal
*/
showSettingsModal() {
if (this.elements.settingsModal) {
this.elements.settingsModal.style.display = 'block';
}
}
/**
* Hide Settings modal
*/
hideSettingsModal() {
if (this.elements.settingsModal) {
this.elements.settingsModal.style.display = 'none';
}
}
/**
* Save API Keys
*/
async saveApiKeys() {
const inputs = this.elements.apiKeyInputs.querySelectorAll('input');
const keys = {};
inputs.forEach(input => {
const provider = input.dataset.provider;
const value = input.value.trim();
if (provider && value) {
keys[provider] = value;
}
});
if (Object.keys(keys).length === 0) {
this.showWarning('No API keys were entered.');
return;
}
try {
const response = await this.apiCall('/api/config/api-keys', 'POST', keys);
if (response.success) {
this.showSuccess(response.message);
this.hideSettingsModal();
this.loadProviders(); // Refresh provider status
} else {
throw new Error(response.error || 'Failed to save API keys');
}
} catch (error) {
this.showError(`Error saving API keys: ${error.message}`);
}
}
/**
* Reset API Key fields
*/
resetApiKeys() {
const inputs = this.elements.apiKeyInputs.querySelectorAll('input');
inputs.forEach(input => {
input.value = '';
});
}
/**
* Check if graph data has changed
* @param {Object} graphData - New graph data
* @returns {boolean} True if data has changed
*/
hasGraphChanged(graphData) {
// Simple check based on node and edge counts and timestamps
const currentStats = this.graphManager.getStatistics();
const newNodeCount = graphData.nodes ? graphData.nodes.length : 0;
const newEdgeCount = graphData.edges ? graphData.edges.length : 0;
// Check if counts changed
const countsChanged = currentStats.nodeCount !== newNodeCount || currentStats.edgeCount !== newEdgeCount;
// Also check if we have new timestamp data
const hasNewTimestamp = graphData.statistics &&
graphData.statistics.last_modified &&
graphData.statistics.last_modified !== this.lastGraphTimestamp;
if (hasNewTimestamp) {
this.lastGraphTimestamp = graphData.statistics.last_modified;
}
const changed = countsChanged || hasNewTimestamp;
console.log(`Graph change check: Current(${currentStats.nodeCount}n, ${currentStats.edgeCount}e) vs New(${newNodeCount}n, ${newEdgeCount}e) = ${changed}`);
return changed;
}
/**
* Make API call to server
* @param {string} endpoint - API endpoint
* @param {string} method - HTTP method
* @param {Object} data - Request data
* @returns {Promise