gradient
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
/**
|
||||
* Graph visualization module for DNScope
|
||||
* Handles network graph rendering using vis.js with proper large entity node hiding
|
||||
* UPDATED: Added manual refresh button for polling optimization when graph becomes large
|
||||
* UPDATED: Added time-based blue gradient edge coloring system
|
||||
*/
|
||||
const contextMenuCSS = `
|
||||
.graph-context-menu {
|
||||
@@ -53,6 +53,44 @@ const contextMenuCSS = `
|
||||
.graph-context-menu ul li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.time-control-container {
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
background: rgba(42, 42, 42, 0.3);
|
||||
border-radius: 4px;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.time-control-label {
|
||||
font-size: 0.8rem;
|
||||
color: #c7c7c7;
|
||||
margin-bottom: 0.3rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.time-control-input {
|
||||
width: 100%;
|
||||
padding: 0.3rem;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #555;
|
||||
border-radius: 3px;
|
||||
color: #c7c7c7;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.time-control-input:focus {
|
||||
outline: none;
|
||||
border-color: #00ff41;
|
||||
}
|
||||
|
||||
.time-gradient-info {
|
||||
font-size: 0.7rem;
|
||||
color: #999;
|
||||
margin-top: 0.3rem;
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
|
||||
class GraphManager {
|
||||
@@ -76,6 +114,16 @@ class GraphManager {
|
||||
this.manualRefreshButton = null;
|
||||
this.manualRefreshHandler = null; // Store the handler
|
||||
|
||||
// Time-based gradient settings
|
||||
this.timeOfInterest = new Date(); // Default to now
|
||||
this.edgeTimestamps = new Map(); // Store edge ID -> timestamp mapping
|
||||
|
||||
// Gradient colors: grey-ish dark to retina-melting light blue
|
||||
this.gradientColors = {
|
||||
dark: '#6b7280', // Grey-ish dark
|
||||
light: '#00bfff' // Retina-melting light blue
|
||||
};
|
||||
|
||||
this.options = {
|
||||
nodes: {
|
||||
shape: 'dot',
|
||||
@@ -257,13 +305,25 @@ class GraphManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add interactive graph controls
|
||||
* UPDATED: Added manual refresh button for polling optimization
|
||||
* Add interactive graph controls with time of interest control
|
||||
* UPDATED: Added time-based edge coloring controls
|
||||
*/
|
||||
addGraphControls() {
|
||||
const controlsContainer = document.createElement('div');
|
||||
controlsContainer.className = 'graph-controls';
|
||||
|
||||
// Format current date/time for the input
|
||||
const currentDateTime = this.formatDateTimeForInput(this.timeOfInterest);
|
||||
|
||||
controlsContainer.innerHTML = `
|
||||
<div class="time-control-container">
|
||||
<label class="time-control-label">Time of Interest (for edge coloring)</label>
|
||||
<input type="datetime-local" id="time-of-interest" class="time-control-input"
|
||||
value="${currentDateTime}" title="Reference time for edge color gradient">
|
||||
<div class="time-gradient-info">
|
||||
Dark: Old data | Light Blue: Recent data
|
||||
</div>
|
||||
</div>
|
||||
<button class="graph-control-btn" id="graph-fit" title="Fit to Screen">[FIT]</button>
|
||||
<button class="graph-control-btn" id="graph-physics" title="Toggle Physics">[PHYSICS]</button>
|
||||
<button class="graph-control-btn" id="graph-cluster" title="Cluster Nodes">[CLUSTER]</button>
|
||||
@@ -283,6 +343,13 @@ class GraphManager {
|
||||
document.getElementById('graph-unhide').addEventListener('click', () => this.unhideAll());
|
||||
document.getElementById('graph-revert').addEventListener('click', () => this.revertLastAction());
|
||||
|
||||
// Time of interest control
|
||||
document.getElementById('time-of-interest').addEventListener('change', (e) => {
|
||||
this.timeOfInterest = new Date(e.target.value);
|
||||
console.log('Time of interest updated:', this.timeOfInterest);
|
||||
this.updateEdgeColors();
|
||||
});
|
||||
|
||||
// Manual refresh button - handler will be set by main app
|
||||
this.manualRefreshButton = document.getElementById('graph-manual-refresh');
|
||||
// If a handler was set before the button existed, attach it now
|
||||
@@ -290,6 +357,150 @@ class GraphManager {
|
||||
this.manualRefreshButton.addEventListener('click', this.manualRefreshHandler);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date for datetime-local input
|
||||
*/
|
||||
formatDateTimeForInput(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract relevant timestamp from edge raw_data based on provider
|
||||
*/
|
||||
extractEdgeTimestamp(edge) {
|
||||
const rawData = edge.raw_data || {};
|
||||
const provider = edge.source_provider || '';
|
||||
|
||||
// Check for standardized relevance_timestamp first
|
||||
if (rawData.relevance_timestamp) {
|
||||
return new Date(rawData.relevance_timestamp);
|
||||
}
|
||||
|
||||
// Provider-specific timestamp extraction
|
||||
switch (provider.toLowerCase()) {
|
||||
case 'shodan':
|
||||
// Use last_seen timestamp for Shodan
|
||||
if (rawData.last_seen) {
|
||||
return new Date(rawData.last_seen);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'crtsh':
|
||||
// Use certificate issue date (not_before) for certificates
|
||||
if (rawData.cert_not_before) {
|
||||
return new Date(rawData.cert_not_before);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'dns':
|
||||
case 'correlation':
|
||||
default:
|
||||
// Use discovery timestamp for DNS and correlation
|
||||
if (edge.discovery_timestamp) {
|
||||
return new Date(edge.discovery_timestamp);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Fallback to discovery timestamp or current time
|
||||
if (edge.discovery_timestamp) {
|
||||
return new Date(edge.discovery_timestamp);
|
||||
}
|
||||
|
||||
return new Date(); // Default to now if no timestamp available
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate time-based blue gradient color
|
||||
*/
|
||||
calculateTimeGradientColor(timestamp) {
|
||||
if (!timestamp || !this.timeOfInterest) {
|
||||
return this.gradientColors.dark; // Default to dark grey
|
||||
}
|
||||
|
||||
// Calculate time difference in milliseconds
|
||||
const timeDiff = Math.abs(timestamp.getTime() - this.timeOfInterest.getTime());
|
||||
|
||||
// Find maximum time difference across all edges for normalization
|
||||
let maxTimeDiff = 0;
|
||||
this.edgeTimestamps.forEach((edgeTimestamp) => {
|
||||
const diff = Math.abs(edgeTimestamp.getTime() - this.timeOfInterest.getTime());
|
||||
if (diff > maxTimeDiff) {
|
||||
maxTimeDiff = diff;
|
||||
}
|
||||
});
|
||||
|
||||
if (maxTimeDiff === 0) {
|
||||
return this.gradientColors.light; // All timestamps are the same
|
||||
}
|
||||
|
||||
// Calculate gradient position (0 = closest to time of interest, 1 = furthest)
|
||||
const gradientPosition = timeDiff / maxTimeDiff;
|
||||
|
||||
// Interpolate between light blue (close) and dark grey (far)
|
||||
return this.interpolateColor(
|
||||
this.gradientColors.light, // Close to time of interest
|
||||
this.gradientColors.dark, // Far from time of interest
|
||||
gradientPosition
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpolate between two hex colors
|
||||
*/
|
||||
interpolateColor(color1, color2, factor) {
|
||||
// Parse hex colors
|
||||
const hex1 = color1.replace('#', '');
|
||||
const hex2 = color2.replace('#', '');
|
||||
|
||||
const r1 = parseInt(hex1.substring(0, 2), 16);
|
||||
const g1 = parseInt(hex1.substring(2, 4), 16);
|
||||
const b1 = parseInt(hex1.substring(4, 6), 16);
|
||||
|
||||
const r2 = parseInt(hex2.substring(0, 2), 16);
|
||||
const g2 = parseInt(hex2.substring(2, 4), 16);
|
||||
const b2 = parseInt(hex2.substring(4, 6), 16);
|
||||
|
||||
// Interpolate
|
||||
const r = Math.round(r1 + (r2 - r1) * factor);
|
||||
const g = Math.round(g1 + (g2 - g1) * factor);
|
||||
const b = Math.round(b1 + (b2 - b1) * factor);
|
||||
|
||||
// Convert back to hex
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update all edge colors based on current time of interest
|
||||
*/
|
||||
updateEdgeColors() {
|
||||
const edgeUpdates = [];
|
||||
|
||||
this.edges.forEach((edge) => {
|
||||
const timestamp = this.edgeTimestamps.get(edge.id);
|
||||
const color = this.calculateTimeGradientColor(timestamp);
|
||||
|
||||
edgeUpdates.push({
|
||||
id: edge.id,
|
||||
color: {
|
||||
color: color,
|
||||
highlight: '#00ff41',
|
||||
hover: '#ff9900'
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (edgeUpdates.length > 0) {
|
||||
this.edges.update(edgeUpdates);
|
||||
console.log(`Updated ${edgeUpdates.length} edge colors based on time gradient`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the manual refresh button click handler
|
||||
@@ -411,6 +622,7 @@ class GraphManager {
|
||||
if (!hasData) {
|
||||
this.nodes.clear();
|
||||
this.edges.clear();
|
||||
this.edgeTimestamps.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -464,6 +676,9 @@ class GraphManager {
|
||||
this.nodes.update(processedNodes);
|
||||
this.edges.update(processedEdges);
|
||||
|
||||
// Update edge timestamps and colors for time-based gradient
|
||||
this.updateEdgeTimestampsAndColors(graphData.edges);
|
||||
|
||||
this.updateFilterControls();
|
||||
this.applyAllFilters();
|
||||
|
||||
@@ -481,6 +696,21 @@ class GraphManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update edge timestamps and apply time-based gradient colors
|
||||
*/
|
||||
updateEdgeTimestampsAndColors(edgeData) {
|
||||
// Extract timestamps from raw edge data
|
||||
edgeData.forEach(edge => {
|
||||
const edgeId = `${edge.from}-${edge.to}-${edge.label}`;
|
||||
const timestamp = this.extractEdgeTimestamp(edge);
|
||||
this.edgeTimestamps.set(edgeId, timestamp);
|
||||
});
|
||||
|
||||
// Update edge colors based on new timestamps
|
||||
this.updateEdgeColors();
|
||||
}
|
||||
|
||||
analyzeCertificateInfo(attributes) {
|
||||
let hasCertificates = false;
|
||||
let hasValidCertificates = false;
|
||||
@@ -558,12 +788,6 @@ class GraphManager {
|
||||
if (node.max_depth_reached) {
|
||||
processedNode.borderColor = '#ff0000'; // Red border for max depth
|
||||
}
|
||||
|
||||
|
||||
// Add confidence-based styling
|
||||
if (node.confidence) {
|
||||
processedNode.borderWidth = Math.max(2, Math.floor(node.confidence * 5));
|
||||
}
|
||||
|
||||
// FIXED: Certificate-based domain coloring
|
||||
if (node.type === 'domain' && Array.isArray(node.attributes)) {
|
||||
@@ -595,24 +819,33 @@ class GraphManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Process edge data with styling and metadata
|
||||
* Process edge data with styling, metadata, and time-based gradient colors
|
||||
* @param {Object} edge - Raw edge data
|
||||
* @returns {Object} Processed edge data
|
||||
*/
|
||||
processEdge(edge) {
|
||||
const confidence = edge.confidence_score || 0;
|
||||
const edgeId = `${edge.from}-${edge.to}-${edge.label}`;
|
||||
|
||||
// Extract timestamp for this edge
|
||||
const timestamp = this.extractEdgeTimestamp(edge);
|
||||
this.edgeTimestamps.set(edgeId, timestamp);
|
||||
|
||||
// Calculate time-based gradient color
|
||||
const timeGradientColor = this.calculateTimeGradientColor(timestamp);
|
||||
|
||||
const processedEdge = {
|
||||
id: `${edge.from}-${edge.to}-${edge.label}`,
|
||||
id: edgeId,
|
||||
from: edge.from,
|
||||
to: edge.to,
|
||||
label: this.formatEdgeLabel(edge.label, confidence),
|
||||
label: edge.label, // Correctly access the label directly
|
||||
title: this.createEdgeTooltip(edge),
|
||||
width: this.getEdgeWidth(confidence),
|
||||
color: this.getEdgeColor(confidence),
|
||||
dashes: confidence < 0.6 ? [5, 5] : false,
|
||||
color: {
|
||||
color: timeGradientColor,
|
||||
highlight: '#00ff41',
|
||||
hover: '#ff9900'
|
||||
},
|
||||
metadata: {
|
||||
relationship_type: edge.label,
|
||||
confidence_score: confidence,
|
||||
source_provider: edge.source_provider,
|
||||
discovery_timestamp: edge.discovery_timestamp
|
||||
}
|
||||
@@ -635,18 +868,7 @@ class GraphManager {
|
||||
return nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format edge label for display
|
||||
* @param {string} relationshipType - Type of relationship
|
||||
* @param {number} confidence - Confidence score
|
||||
* @returns {string} Formatted label
|
||||
*/
|
||||
formatEdgeLabel(relationshipType, confidence) {
|
||||
if (!relationshipType) return '';
|
||||
|
||||
const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '●' : '○';
|
||||
return `${relationshipType} ${confidenceText}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node color based on type
|
||||
@@ -716,44 +938,13 @@ class GraphManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edge color based on confidence
|
||||
* @param {number} confidence - Confidence score
|
||||
* @returns {string} Edge color
|
||||
*/
|
||||
getEdgeColor(confidence) {
|
||||
if (confidence >= 0.8) {
|
||||
return '#00ff41'; // High confidence - green
|
||||
} else if (confidence >= 0.6) {
|
||||
return '#ff9900'; // Medium confidence - amber
|
||||
} else {
|
||||
return '#666666'; // Low confidence - gray
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get edge width based on confidence
|
||||
* @param {number} confidence - Confidence score
|
||||
* @returns {number} Edge width
|
||||
*/
|
||||
getEdgeWidth(confidence) {
|
||||
if (confidence >= 0.8) {
|
||||
return 3;
|
||||
} else if (confidence >= 0.6) {
|
||||
return 2;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create edge tooltip with correct provider information
|
||||
* Create edge tooltip with correct provider information and timestamp
|
||||
* @param {Object} edge - Edge data
|
||||
* @returns {string} HTML tooltip content
|
||||
*/
|
||||
createEdgeTooltip(edge) {
|
||||
let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
|
||||
tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
|
||||
tooltip += `<div style="color: #999; margin-bottom: 2px;">Confidence: ${(edge.confidence_score * 100).toFixed(1)}%</div>`;
|
||||
|
||||
if (edge.source_provider) {
|
||||
tooltip += `<div style="color: #999; margin-bottom: 2px;">Provider: ${edge.source_provider}</div>`;
|
||||
@@ -764,6 +955,13 @@ class GraphManager {
|
||||
tooltip += `<div style="color: #666; font-size: 10px;">Discovered: ${date.toLocaleString()}</div>`;
|
||||
}
|
||||
|
||||
// Add timestamp information for time-based coloring
|
||||
const edgeId = `${edge.from}-${edge.to}-${edge.label}`;
|
||||
const timestamp = this.edgeTimestamps.get(edgeId);
|
||||
if (timestamp) {
|
||||
tooltip += `<div style="color: #888; font-size: 10px;">Data from: ${timestamp.toLocaleString()}</div>`;
|
||||
}
|
||||
|
||||
tooltip += `</div>`;
|
||||
return tooltip;
|
||||
}
|
||||
@@ -893,13 +1091,17 @@ class GraphManager {
|
||||
};
|
||||
});
|
||||
|
||||
// Reset highlighted edges
|
||||
// Reset highlighted edges to time-based colors
|
||||
const edgeUpdates = this.highlightedElements.edges.map(id => {
|
||||
const originalEdge = this.edges.get(id);
|
||||
const timestamp = this.edgeTimestamps.get(id);
|
||||
const color = this.calculateTimeGradientColor(timestamp);
|
||||
return {
|
||||
id: id,
|
||||
color: this.getEdgeColor(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5),
|
||||
width: this.getEdgeWidth(originalEdge.metadata ? originalEdge.metadata.confidence_score : 0.5)
|
||||
color: {
|
||||
color: color,
|
||||
highlight: '#00ff41',
|
||||
hover: '#ff9900'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -955,11 +1157,19 @@ class GraphManager {
|
||||
borderWidth: 2,
|
||||
}));
|
||||
|
||||
const edgeResets = newEdges.map(edge => ({
|
||||
id: edge.id,
|
||||
color: this.getEdgeColor(edge.metadata ? edge.metadata.confidence_score : 0.5),
|
||||
width: this.getEdgeWidth(edge.metadata ? edge.metadata.confidence_score : 0.5)
|
||||
}));
|
||||
// Reset edges to time-based colors
|
||||
const edgeResets = newEdges.map(edge => {
|
||||
const timestamp = this.edgeTimestamps.get(edge.id);
|
||||
const color = this.calculateTimeGradientColor(timestamp);
|
||||
return {
|
||||
id: edge.id,
|
||||
color: {
|
||||
color: color,
|
||||
highlight: '#00ff41',
|
||||
hover: '#ff9900'
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.nodes.update(nodeResets);
|
||||
this.edges.update(edgeResets);
|
||||
@@ -1048,6 +1258,7 @@ class GraphManager {
|
||||
clear() {
|
||||
this.nodes.clear();
|
||||
this.edges.clear();
|
||||
this.edgeTimestamps.clear();
|
||||
this.history = [];
|
||||
this.largeEntityMembers.clear();
|
||||
this.initialTargetIds.clear();
|
||||
|
||||
@@ -1722,17 +1722,9 @@ class DNScopeApp {
|
||||
return groups;
|
||||
}
|
||||
|
||||
formatEdgeLabel(relationshipType, confidence) {
|
||||
if (!relationshipType) return '';
|
||||
|
||||
const confidenceText = confidence >= 0.8 ? '●' : confidence >= 0.6 ? '◐' : '○';
|
||||
return `${relationshipType} ${confidenceText}`;
|
||||
}
|
||||
|
||||
createEdgeTooltip(edge) {
|
||||
let tooltip = `<div style="font-family: 'Roboto Mono', monospace; font-size: 11px;">`;
|
||||
tooltip += `<div style="color: #00ff41; font-weight: bold; margin-bottom: 4px;">${edge.label || 'Relationship'}</div>`;
|
||||
tooltip += `<div style="color: #999; margin-bottom: 2px;">Confidence: ${(edge.confidence_score * 100).toFixed(1)}%</div>`;
|
||||
|
||||
// UPDATED: Use raw provider name (no formatting)
|
||||
if (edge.source_provider) {
|
||||
@@ -1872,7 +1864,7 @@ class DNScopeApp {
|
||||
html += `
|
||||
<div class="relationship-compact-item">
|
||||
<span class="node-link-compact" data-node-id="${innerNodeId}">${innerNodeId}</span>
|
||||
<button class="btn-icon-small extract-node-btn"
|
||||
<button class="graph-control-btn extract-node-btn"
|
||||
title="Extract to graph"
|
||||
data-large-entity-id="${largeEntityId}"
|
||||
data-node-id="${innerNodeId}">[+]</button>
|
||||
@@ -1899,8 +1891,6 @@ class DNScopeApp {
|
||||
`;
|
||||
|
||||
node.incoming_edges.forEach(edge => {
|
||||
const confidence = edge.data.confidence_score || 0;
|
||||
const confidenceClass = confidence >= 0.8 ? 'high' : confidence >= 0.6 ? 'medium' : 'low';
|
||||
|
||||
html += `
|
||||
<div class="relationship-item">
|
||||
@@ -1909,9 +1899,6 @@ class DNScopeApp {
|
||||
</div>
|
||||
<div class="relationship-type">
|
||||
<span class="relation-label">${edge.data.relationship_type}</span>
|
||||
<span class="confidence-indicator confidence-${confidenceClass}" title="Confidence: ${(confidence * 100).toFixed(1)}%">
|
||||
${'●'.repeat(Math.ceil(confidence * 3))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1930,9 +1917,6 @@ class DNScopeApp {
|
||||
`;
|
||||
|
||||
node.outgoing_edges.forEach(edge => {
|
||||
const confidence = edge.data.confidence_score || 0;
|
||||
const confidenceClass = confidence >= 0.8 ? 'high' : confidence >= 0.6 ? 'medium' : 'low';
|
||||
|
||||
html += `
|
||||
<div class="relationship-item">
|
||||
<div class="relationship-target node-link" data-node-id="${edge.to}">
|
||||
@@ -1940,9 +1924,6 @@ class DNScopeApp {
|
||||
</div>
|
||||
<div class="relationship-type">
|
||||
<span class="relation-label">${edge.data.relationship_type}</span>
|
||||
<span class="confidence-indicator confidence-${confidenceClass}" title="Confidence: ${(confidence * 100).toFixed(1)}%">
|
||||
${'●'.repeat(Math.ceil(confidence * 3))}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user