data model refinement
This commit is contained in:
@@ -581,30 +581,6 @@ input[type="text"]:focus, select:focus {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
margin: 5% auto;
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
animation: slideInDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideInDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -616,43 +592,6 @@ input[type="text"]:focus, select:focus {
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
background-color: #1a1a1a;
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid #444;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
color: #00ff41;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #c7c7c7;
|
||||
font-size: 1.2rem;
|
||||
cursor: pointer;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: #ff9900;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-description {
|
||||
color: #999;
|
||||
margin-bottom: 1.5rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -801,12 +740,6 @@ input[type="text"]:focus, select:focus {
|
||||
color: #00ff41 !important;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
@@ -970,4 +903,104 @@ input[type="text"]:focus, select:focus {
|
||||
.large-entity-node-details .detail-section-header {
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
/* dnsrecon/static/css/main.css */
|
||||
|
||||
/* --- Add these styles for the modal --- */
|
||||
|
||||
.modal {
|
||||
display: none; /* Hidden by default */
|
||||
position: fixed; /* Stay in place */
|
||||
z-index: 1000; /* Sit on top */
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto; /* Enable scroll if needed */
|
||||
background-color: rgba(0,0,0,0.6); /* Black w/ opacity */
|
||||
backdrop-filter: blur(5px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #1e1e1e;
|
||||
margin: 10% auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #444;
|
||||
width: 60%;
|
||||
max-width: 800px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #444;
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-family: 'Special Elite', monospace;
|
||||
color: #00ff41;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #c7c7c7;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 0 10px;
|
||||
}
|
||||
.modal-close:hover {
|
||||
color: #ff6b6b;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
max-height: 60vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Styles for the new data model display */
|
||||
.modal-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.modal-section h4 {
|
||||
font-family: 'Special Elite', monospace;
|
||||
color: #ff9900;
|
||||
border-bottom: 1px dashed #555;
|
||||
padding-bottom: 5px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.modal-section ul {
|
||||
list-style-type: none;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.modal-section li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.modal-section li > ul {
|
||||
padding-left: 20px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.description-text, .no-data {
|
||||
color: #aaa;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {opacity: 0; transform: scale(0.95);}
|
||||
to {opacity: 1; transform: scale(1);}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user