This commit is contained in:
overcuriousity
2025-09-24 09:30:42 +02:00
parent 571912218e
commit 897bb80183
15 changed files with 541 additions and 335 deletions

View File

@@ -326,6 +326,20 @@ input[type="text"]:focus, select:focus {
animation: progressGlow 2s ease-in-out infinite alternate;
}
.gradient-bar {
height: 4px;
background: linear-gradient(to right, #6b7280, #00bfff);
border-radius: 2px;
margin: 0.2rem 0;
}
.gradient-labels {
display: flex;
justify-content: space-between;
font-size: 0.6rem;
color: #888;
}
@keyframes progressShimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
@@ -380,32 +394,59 @@ input[type="text"]:focus, select:focus {
color: #999;
}
/* Graph Controls */
/* Enhanced graph controls layout */
.graph-controls {
position: absolute;
top: 8px;
right: 8px;
z-index: 10;
display: flex;
flex-direction: column;
gap: 0.3rem;
position: absolute;
top: 10px;
left: 10px;
background: rgba(26, 26, 26, 0.9);
padding: 0.5rem;
border-radius: 6px;
border: 1px solid #444;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
z-index: 100;
min-width: 200px;
}
.graph-control-btn, .btn-icon-small {
background: rgba(42, 42, 42, 0.9);
.graph-control-btn {
background: linear-gradient(135deg, #2a2a2a 0%, #1e1e1e 100%);
border: 1px solid #555;
color: #c7c7c7;
padding: 0.3rem 0.5rem;
font-family: 'Roboto Mono', monospace;
font-size: 0.7rem;
padding: 0.4rem 0.8rem;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
font-family: 'Roboto Mono', monospace;
font-size: 0.8rem;
transition: all 0.2s ease;
text-align: center;
}
.graph-control-btn:hover, .btn-icon-small:hover {
.graph-control-btn:hover {
background: linear-gradient(135deg, #3a3a3a 0%, #2e2e2e 100%);
border-color: #00ff41;
color: #00ff41;
}
.graph-control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.manual-refresh-btn {
background: linear-gradient(135deg, #4a4a2a 0%, #3e3e1e 100%);
border-color: #ffaa00;
color: #ffaa00;
}
.manual-refresh-btn:hover {
background: linear-gradient(135deg, #5a5a3a 0%, #4e4e2e 100%);
color: #ffcc33;
border-color: #ffcc33;
}
.graph-filter-panel {
position: absolute;
bottom: 8px;
@@ -500,14 +541,6 @@ input[type="text"]:focus, select:focus {
height: 2px;
}
.legend-edge.high-confidence {
background: #00ff41;
}
.legend-edge.medium-confidence {
background: #ff9900;
}
/* Provider Panel */
.provider-panel {
grid-area: providers;
@@ -987,11 +1020,6 @@ input[type="text"]:focus, select:focus {
border-radius: 2px;
}
.confidence-indicator {
font-size: 0.6rem;
letter-spacing: 1px;
}
.node-link-compact {
color: #00aaff;
text-decoration: none;
@@ -1095,6 +1123,56 @@ input[type="text"]:focus, select:focus {
border-left: 3px solid #00aaff;
}
.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;
font-family: 'Roboto Mono', monospace;
}
.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;
box-shadow: 0 0 5px rgba(0, 255, 65, 0.3);
}
.time-gradient-info {
font-size: 0.7rem;
color: #999;
margin-top: 0.3rem;
text-align: center;
font-family: 'Roboto Mono', monospace;
}
/* Edge color legend for time-based gradient */
.time-gradient-legend {
margin-top: 0.5rem;
padding: 0.3rem;
background: rgba(26, 26, 26, 0.5);
border-radius: 3px;
border: 1px solid #333;
}
/* Settings Modal Specific */
.provider-toggle {
appearance: none !important;
@@ -1324,16 +1402,16 @@ input[type="password"]:focus {
.provider-list {
grid-template-columns: 1fr;
}
}
.manual-refresh-btn {
background: rgba(92, 76, 44, 0.9) !important; /* Orange/amber background */
border: 1px solid #7a6a3a !important;
color: #ffcc00 !important; /* Bright yellow text */
}
.manual-refresh-btn:hover {
border-color: #ffcc00 !important;
color: #fff !important;
background: rgba(112, 96, 54, 0.9) !important;
.graph-controls {
position: relative;
top: auto;
left: auto;
margin-bottom: 1rem;
min-width: auto;
}
.time-control-input {
font-size: 0.7rem;
}
}

View File

@@ -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();

View File

@@ -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>
`;