data model refinement

This commit is contained in:
overcuriousity
2025-09-13 21:10:27 +02:00
parent 930fdca500
commit 2974312278
5 changed files with 231 additions and 230 deletions

View File

@@ -213,12 +213,12 @@ class GraphManager {
}
});
// TODO Context menu (right-click)
// FIX: Comment out the problematic context menu handler
this.network.on('oncontext', (params) => {
params.event.preventDefault();
if (params.nodes.length > 0) {
this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]);
}
// if (params.nodes.length > 0) {
// this.showNodeContextMenu(params.pointer.DOM, params.nodes[0]);
// }
});
// Stabilization events with progress
@@ -256,8 +256,8 @@ class GraphManager {
const largeEntityMap = new Map();
graphData.nodes.forEach(node => {
if (node.type === 'large_entity' && node.metadata && Array.isArray(node.metadata.nodes)) {
node.metadata.nodes.forEach(nodeId => {
if (node.type === 'large_entity' && node.attributes && Array.isArray(node.attributes.nodes)) {
node.attributes.nodes.forEach(nodeId => {
largeEntityMap.set(nodeId, node.id);
});
}
@@ -274,12 +274,14 @@ class GraphManager {
const mergedEdges = {};
graphData.edges.forEach(edge => {
const fromNode = largeEntityMap.has(edge.from) ? largeEntityMap.get(edge.from) : edge.from;
const mergeKey = `${fromNode}-${edge.to}-${edge.label}`;
const toNode = largeEntityMap.has(edge.to) ? largeEntityMap.get(edge.to) : edge.to;
const mergeKey = `${fromNode}-${toNode}-${edge.label}`;
if (!mergedEdges[mergeKey]) {
mergedEdges[mergeKey] = {
...edge,
from: fromNode,
to: toNode,
count: 0,
confidence_score: 0
};
@@ -341,6 +343,8 @@ class GraphManager {
size: this.getNodeSize(node.type),
borderColor: this.getNodeBorderColor(node.type),
shape: this.getNodeShape(node.type),
attributes: node.attributes || {},
description: node.description || '',
metadata: node.metadata || {},
type: node.type
};
@@ -352,12 +356,8 @@ class GraphManager {
// Style based on certificate validity
if (node.type === 'domain') {
if (node.metadata && node.metadata.certificate_data && node.metadata.certificate_data.has_valid_cert === true) {
processedNode.color = '#00ff41'; // Bright green for valid cert
processedNode.borderColor = '#00aa2e';
} else if (node.metadata && node.metadata.certificate_data && node.metadata.certificate_data.has_valid_cert === false) {
processedNode.color = '#888888'; // Muted grey color
processedNode.borderColor = '#666666'; // Darker grey border
if (node.attributes && node.attributes.certificates && node.attributes.certificates.has_valid_cert === false) {
processedNode.color = { background: '#888888', border: '#666666' };
}
}
@@ -404,7 +404,7 @@ class GraphManager {
* @returns {string} Formatted label
*/
formatNodeLabel(nodeId, nodeType) {
// Truncate long domain names
if (typeof nodeId !== 'string') return '';
if (nodeId.length > 20) {
return nodeId.substring(0, 17) + '...';
}
@@ -564,7 +564,7 @@ class GraphManager {
// Trigger custom event for main application to handle
const event = new CustomEvent('nodeSelected', {
detail: { nodeId, node }
detail: { node }
});
document.dispatchEvent(event);
}

View File

@@ -193,9 +193,9 @@ class DNSReconApp {
this.elements.resetApiKeys.addEventListener('click', () => this.resetApiKeys());
}
// Custom events
// ** FIX: Listen for the custom event from the graph **
document.addEventListener('nodeSelected', (e) => {
this.showNodeModal(e.detail.nodeId, e.detail.node);
this.showNodeModal(e.detail.node);
});
// Keyboard shortcuts
@@ -793,129 +793,86 @@ class DNSReconApp {
}
/**
* Generates the HTML for the node details view.
* **FIX**: Generates the HTML for the node details view using the new data model.
* @param {Object} node - The node object.
* @returns {string} The HTML string for the node details.
*/
generateNodeDetailsHtml(node) {
if(!node) return '<div class="detail-row"><span class="detail-value">Details not available.</span></div>';
let detailsHtml = '';
const createDetailRow = (label, value, statusIcon = '') => {
const baseId = `detail-${node.id.replace(/[^a-zA-Z0-9]/g, '-')}-${label.replace(/[^a-zA-Z0-9]/g, '-')}`;
let detailsHtml = '<div class="modal-details-grid">';
if (value === null || value === undefined ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0)) {
return `
<div class="detail-row">
<span class="detail-label">${label} <span class="status-icon text-warning">✗</span></span>
<span class="detail-value">N/A</span>
</div>
`;
}
// Section for Attributes
detailsHtml += '<div class="modal-section">';
detailsHtml += '<h4>Attributes</h4>';
detailsHtml += this.formatObjectToHtml(node.attributes);
detailsHtml += '</div>';
if (Array.isArray(value)) {
return value.map((item, index) => {
const itemId = `${baseId}-${index}`;
const itemLabel = index === 0 ? `${label} <span class="status-icon text-success">✓</span>` : '';
return `
<div class="detail-row">
<span class="detail-label">${itemLabel}</span>
<span class="detail-value" id="${itemId}">${this.formatValue(item)}</span>
<button class="copy-btn" onclick="copyToClipboard('${itemId}')" title="Copy">📋</button>
</div>
`;
}).join('');
} else {
const valueId = `${baseId}-0`;
const icon = statusIcon || '<span class="status-icon text-success">✓</span>';
return `
<div class="detail-row">
<span class="detail-label">${label} ${icon}</span>
<span class="detail-value" id="${valueId}">${this.formatValue(value)}</span>
<button class="copy-btn" onclick="copyToClipboard('${valueId}')" title="Copy">📋</button>
</div>
`;
}
};
// Section for Description
detailsHtml += '<div class="modal-section">';
detailsHtml += '<h4>Description</h4>';
detailsHtml += `<p class="description-text">${node.description || 'No description available.'}</p>`;
detailsHtml += '</div>';
const metadata = node.metadata || {};
detailsHtml += createDetailRow('Node Descriptor', node.id);
switch (node.type) {
case 'domain':
detailsHtml += createDetailRow('DNS Records', metadata.dns_records);
detailsHtml += createDetailRow('Related Domains (SAN)', metadata.related_domains_san);
detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns);
detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
break;
case 'ip':
detailsHtml += createDetailRow('Hostnames', metadata.hostnames);
detailsHtml += createDetailRow('Passive DNS', metadata.passive_dns);
detailsHtml += createDetailRow('Shodan Data', metadata.shodan);
break;
case 'correlation_object':
detailsHtml += createDetailRow('Correlated Value', metadata.value);
if (metadata.correlated_nodes) {
detailsHtml += createDetailRow('Correlated Nodes', metadata.correlated_nodes.join(', '));
}
if (metadata.sources) {
detailsHtml += `<div class="detail-section-header">Correlation Sources</div>`;
for (const source of metadata.sources) {
detailsHtml += createDetailRow(source.node_id, source.path);
}
}
break;
}
if (metadata.certificate_data && Object.keys(metadata.certificate_data).length > 0) {
const cert = metadata.certificate_data;
detailsHtml += `<div class="detail-section-header">Certificate Summary</div>`;
detailsHtml += createDetailRow('Total Found', cert.total_certificates);
detailsHtml += createDetailRow('Currently Valid', cert.valid_certificates);
detailsHtml += createDetailRow('Expires Soon (<30d)', cert.expires_soon_count);
detailsHtml += createDetailRow('Unique Issuers', cert.unique_issuers ? cert.unique_issuers.join(', ') : 'N/A');
if (cert.latest_certificate) {
detailsHtml += `<div class="detail-section-header">Latest Certificate</div>`;
detailsHtml += createDetailRow('Common Name', cert.latest_certificate.common_name);
detailsHtml += createDetailRow('Issuer', cert.latest_certificate.issuer_name);
detailsHtml += createDetailRow('Valid From', new Date(cert.latest_certificate.not_before).toLocaleString());
detailsHtml += createDetailRow('Valid Until', new Date(cert.latest_certificate.not_after).toLocaleString());
}
}
if (metadata.asn_data && Object.keys(metadata.asn_data).length > 0) {
detailsHtml += `<div class="detail-section-header">ASN Information</div>`;
detailsHtml += createDetailRow('ASN', metadata.asn_data.asn);
detailsHtml += createDetailRow('Organization', metadata.asn_data.description);
detailsHtml += createDetailRow('ISP', metadata.asn_data.isp);
detailsHtml += createDetailRow('Country', metadata.asn_data.country);
}
// Section for Metadata
detailsHtml += '<div class="modal-section">';
detailsHtml += '<h4>Metadata</h4>';
detailsHtml += this.formatObjectToHtml(node.metadata);
detailsHtml += '</div>';
detailsHtml += '</div>';
return detailsHtml;
}
/**
* Recursively formats a JavaScript object into an HTML unordered list.
* @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 '<p class="no-data">No data available.</p>';
}
let html = '<ul>';
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());
html += `<li><strong>${formattedKey}:</strong>`;
if (Array.isArray(value)) {
html += `<ul>${value.map(item => `<li>${this.formatValue(item)}</li>`).join('')}</ul>`;
} else if (typeof value === 'object' && value !== null) {
html += this.formatObjectToHtml(value);
} else {
html += ` ${this.formatValue(value)}`;
}
html += '</li>';
}
}
html += '</ul>';
return html;
}
/**
* Show node details modal
* @param {string} nodeId - Node identifier
* @param {Object} node - Node data
*/
showNodeModal(nodeId, node) {
if (!this.elements.nodeModal) return;
showNodeModal(node) {
if (!this.elements.nodeModal || !node) return;
if (this.elements.modalTitle) {
this.elements.modalTitle.textContent = `Node Details`;
this.elements.modalTitle.textContent = `${this.formatStatus(node.type)} Node: ${node.id}`;
}
let detailsHtml = '';
if (node.type === 'large_entity') {
const metadata = node.metadata || {};
const nodes = metadata.nodes || [];
const node_type = metadata.node_type || 'nodes';
detailsHtml += `<div class="detail-section-header">Contains ${metadata.count} ${node_type}s</div>`;
const attributes = node.attributes || {};
const nodes = attributes.nodes || [];
const node_type = attributes.node_type || 'nodes';
detailsHtml += `<div class="detail-section-header">Contains ${attributes.count} ${node_type}s</div>`;
detailsHtml += '<div class="large-entity-nodes-list">';
for(const innerNodeId of nodes) {
@@ -926,12 +883,10 @@ class DNSReconApp {
detailsHtml += `</details>`;
}
detailsHtml += '</div>';
} else {
detailsHtml = this.generateNodeDetailsHtml(node);
}
if (this.elements.modalDetails) {
this.elements.modalDetails.innerHTML = detailsHtml;
}