data-model #2

Merged
mstoeck3 merged 20 commits from data-model into main 2025-09-17 21:56:18 +00:00
2 changed files with 372 additions and 33 deletions
Showing only changes of commit 229746e1ec - Show all commits

View File

@ -56,15 +56,22 @@ class GraphManager:
self.date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}') self.date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}')
def process_correlations_for_node(self, node_id: str): def process_correlations_for_node(self, node_id: str):
"""Process correlations for a given node based on its attributes.""" """
UPDATED: Process correlations for a given node with enhanced tracking.
Now properly tracks which attribute/provider created each correlation.
"""
if not self.graph.has_node(node_id): if not self.graph.has_node(node_id):
return return
node_attributes = self.graph.nodes[node_id].get('attributes', []) node_attributes = self.graph.nodes[node_id].get('attributes', [])
# Process each attribute for potential correlations
for attr in node_attributes: for attr in node_attributes:
attr_name = attr.get('name') attr_name = attr.get('name')
attr_value = attr.get('value') attr_value = attr.get('value')
attr_provider = attr.get('provider', 'unknown')
# Skip excluded attributes and invalid values
if attr_name in self.EXCLUDED_KEYS or not isinstance(attr_value, (str, int, float, bool)) or attr_value is None: if attr_name in self.EXCLUDED_KEYS or not isinstance(attr_value, (str, int, float, bool)) or attr_value is None:
continue continue
@ -74,25 +81,91 @@ class GraphManager:
if isinstance(attr_value, str) and (len(attr_value) < 4 or self.date_pattern.match(attr_value)): if isinstance(attr_value, str) and (len(attr_value) < 4 or self.date_pattern.match(attr_value)):
continue continue
# Initialize correlation tracking for this value
if attr_value not in self.correlation_index: if attr_value not in self.correlation_index:
self.correlation_index[attr_value] = set() self.correlation_index[attr_value] = {
'nodes': set(),
'sources': [] # Track which provider/attribute combinations contributed
}
self.correlation_index[attr_value].add(node_id) # Add this node and source information
self.correlation_index[attr_value]['nodes'].add(node_id)
if len(self.correlation_index[attr_value]) > 1: # Track the source of this correlation value
self._create_correlation_node_and_edges(attr_value, self.correlation_index[attr_value]) source_info = {
'node_id': node_id,
'provider': attr_provider,
'attribute': attr_name,
'path': f"{attr_provider}_{attr_name}"
}
def _create_correlation_node_and_edges(self, value, nodes): # Add source if not already present (avoid duplicates)
"""Create a correlation node and edges to the correlated nodes.""" existing_sources = [s for s in self.correlation_index[attr_value]['sources']
correlation_node_id = f"corr_{value}" if s['node_id'] == node_id and s['path'] == source_info['path']]
if not existing_sources:
self.correlation_index[attr_value]['sources'].append(source_info)
# Create correlation node if we have multiple nodes with this value
if len(self.correlation_index[attr_value]['nodes']) > 1:
self._create_enhanced_correlation_node_and_edges(attr_value, self.correlation_index[attr_value])
def _create_enhanced_correlation_node_and_edges(self, value, correlation_data):
"""
UPDATED: Create correlation node and edges with detailed provider tracking.
"""
correlation_node_id = f"corr_{hash(str(value)) & 0x7FFFFFFF}"
nodes = correlation_data['nodes']
sources = correlation_data['sources']
# Create or update correlation node
if not self.graph.has_node(correlation_node_id): if not self.graph.has_node(correlation_node_id):
self.add_node(correlation_node_id, NodeType.CORRELATION_OBJECT, # Determine the most common provider/attribute combination
metadata={'value': value, 'correlated_nodes': list(nodes)}) provider_counts = {}
for source in sources:
key = f"{source['provider']}_{source['attribute']}"
provider_counts[key] = provider_counts.get(key, 0) + 1
# Use the most common provider/attribute as the primary label
primary_source = max(provider_counts.items(), key=lambda x: x[1])[0] if provider_counts else "unknown_correlation"
metadata = {
'value': value,
'correlated_nodes': list(nodes),
'sources': sources,
'primary_source': primary_source,
'correlation_count': len(nodes)
}
self.add_node(correlation_node_id, NodeType.CORRELATION_OBJECT, metadata=metadata)
print(f"Created correlation node {correlation_node_id} for value '{value}' with {len(nodes)} nodes")
# Create edges from each node to the correlation node
for source in sources:
node_id = source['node_id']
provider = source['provider']
attribute = source['attribute']
for node_id in nodes:
if self.graph.has_node(node_id) and not self.graph.has_edge(node_id, correlation_node_id): if self.graph.has_node(node_id) and not self.graph.has_edge(node_id, correlation_node_id):
self.add_edge(node_id, correlation_node_id, "correlation", confidence_score=0.9) # Format relationship label as "provider: attribute"
display_provider = provider
display_attribute = attribute.replace('_', ' ').replace('cert ', '').strip()
relationship_label = f"{display_provider}: {display_attribute}"
self.add_edge(
source_id=node_id,
target_id=correlation_node_id,
relationship_type=relationship_label,
confidence_score=0.9,
source_provider=provider,
raw_data={
'correlation_value': value,
'original_attribute': attribute,
'correlation_type': 'attribute_matching'
}
)
print(f"Added correlation edge: {node_id} -> {correlation_node_id} ({relationship_label})")
def add_node(self, node_id: str, node_type: NodeType, attributes: Optional[List[Dict[str, Any]]] = None, def add_node(self, node_id: str, node_type: NodeType, attributes: Optional[List[Dict[str, Any]]] = None,
description: str = "", metadata: Optional[Dict[str, Any]] = None) -> bool: description: str = "", metadata: Optional[Dict[str, Any]] = None) -> bool:
@ -335,7 +408,7 @@ class GraphManager:
def get_graph_data(self) -> Dict[str, Any]: def get_graph_data(self) -> Dict[str, Any]:
""" """
Export graph data formatted for frontend visualization. Export graph data formatted for frontend visualization.
Compatible with unified data model - preserves all attribute information for frontend display. UPDATED: Fixed certificate validity styling logic for unified data model.
""" """
nodes = [] nodes = []
for node_id, attrs in self.graph.nodes(data=True): for node_id, attrs in self.graph.nodes(data=True):
@ -345,16 +418,46 @@ class GraphManager:
'metadata': attrs.get('metadata', {}), 'metadata': attrs.get('metadata', {}),
'added_timestamp': attrs.get('added_timestamp')} 'added_timestamp': attrs.get('added_timestamp')}
# Customize node appearance based on type and attributes # UPDATED: Fixed certificate validity styling logic
node_type = node_data['type'] node_type = node_data['type']
attributes_list = node_data['attributes'] attributes_list = node_data['attributes']
# CORRECTED LOGIC: Handle certificate validity styling
if node_type == 'domain' and isinstance(attributes_list, list): if node_type == 'domain' and isinstance(attributes_list, list):
# Find the certificates attribute in the list # Check for certificate-related attributes
cert_attr = next((attr for attr in attributes_list if attr.get('name') == 'certificates'), None) has_certificates = False
if cert_attr and cert_attr.get('value', {}).get('has_valid_cert') is False: has_valid_certificates = False
node_data['color'] = {'background': '#c7c7c7', 'border': '#999'} # Gray for invalid cert has_expired_certificates = False
for attr in attributes_list:
attr_name = attr.get('name', '').lower()
attr_provider = attr.get('provider', '').lower()
attr_value = attr.get('value')
# Look for certificate attributes from crt.sh provider
if attr_provider == 'crtsh' or 'cert' in attr_name:
has_certificates = True
# Check certificate validity
if attr_name == 'cert_is_currently_valid':
if attr_value is True:
has_valid_certificates = True
elif attr_value is False:
has_expired_certificates = True
# Also check for certificate expiry indicators
elif 'expires_soon' in attr_name and attr_value is True:
has_expired_certificates = True
elif 'expired' in attr_name and attr_value is True:
has_expired_certificates = True
# Apply styling based on certificate status
if has_expired_certificates and not has_valid_certificates:
# Red for expired/invalid certificates
node_data['color'] = {'background': '#ff6b6b', 'border': '#cc5555'}
elif not has_certificates:
# Grey for domains with no certificates
node_data['color'] = {'background': '#c7c7c7', 'border': '#999999'}
# Default green styling is handled by the frontend for domains with valid certificates
# Add incoming and outgoing edges to node data # Add incoming and outgoing edges to node data
if self.graph.has_node(node_id): if self.graph.has_node(node_id):

View File

@ -822,8 +822,8 @@ class DNSReconApp {
} }
/** /**
* UPDATED: Enhanced node details HTML generation for unified data model * Node details HTML generation for unified data model
* Now processes StandardAttribute objects instead of simple key-value pairs * processes StandardAttribute objects
*/ */
generateNodeDetailsHtml(node) { generateNodeDetailsHtml(node) {
if (!node) return '<div class="detail-row"><span class="detail-value">Details not available.</span></div>'; if (!node) return '<div class="detail-row"><span class="detail-value">Details not available.</span></div>';
@ -871,7 +871,7 @@ class DNSReconApp {
} }
/** /**
* UPDATED: Generate details for standard nodes using unified data model * Generate details for standard nodes using unified data model
*/ */
generateStandardNodeDetails(node) { generateStandardNodeDetails(node) {
let html = ''; let html = '';
@ -879,9 +879,9 @@ class DNSReconApp {
// Relationships sections // Relationships sections
html += this.generateRelationshipsSection(node); html += this.generateRelationshipsSection(node);
// UPDATED: Simplified attributes section for the flat model // Attributes section with grouping
if (node.attributes && Array.isArray(node.attributes) && node.attributes.length > 0) { if (node.attributes && Array.isArray(node.attributes) && node.attributes.length > 0) {
html += this.generateAttributesSection(node.attributes); html += this.generateEnhancedAttributesSection(node.attributes, node.type);
} }
// Description section // Description section
@ -893,6 +893,242 @@ class DNSReconApp {
return html; return html;
} }
generateEnhancedAttributesSection(attributes, nodeType) {
if (!Array.isArray(attributes) || attributes.length === 0) {
return '';
}
// Group attributes by provider and type for better organization
const groupedAttributes = this.groupAttributesIntelligently(attributes, nodeType);
let html = '';
for (const [groupName, groupData] of Object.entries(groupedAttributes)) {
const isDefaultOpen = groupData.priority === 'high';
html += `
<div class="modal-section">
<details ${isDefaultOpen ? 'open' : ''}>
<summary>
<span>${groupData.icon} ${groupName}</span>
<span class="count-badge">${groupData.attributes.length}</span>
</summary>
<div class="modal-section-content">
<div class="attribute-list">
`;
groupData.attributes.forEach(attr => {
html += `
<div class="attribute-item-compact">
<span class="attribute-key-compact">${this.formatAttributeLabel(attr.name)}</span>
<span class="attribute-value-compact">${this.formatAttributeValueEnhanced(attr)}</span>
</div>
`;
});
html += '</div></div></details></div>';
}
return html;
}
formatAttributeValueEnhanced(attr) {
const value = attr.value;
if (value === null || value === undefined) {
return '<em>None</em>';
}
if (Array.isArray(value)) {
if (value.length === 0) return '<em>None</em>';
if (value.length === 1) return this.escapeHtml(String(value[0]));
// Complex array - make it collapsible
const previewItems = value.slice(0, 2);
const hasMore = value.length > 2;
let html = '<div class="expandable-array">';
html += `<div class="array-preview">`;
previewItems.forEach(item => {
html += `<div class="array-item-preview">${this.escapeHtml(String(item))}</div>`;
});
if (hasMore) {
html += `
<button class="expand-array-btn" onclick="this.parentElement.style.display='none'; this.parentElement.nextElementSibling.style.display='block';">
+${value.length - 2} more...
</button>
`;
}
html += '</div>';
if (hasMore) {
html += `<div class="array-full" style="display: none;">`;
value.forEach(item => {
html += `<div class="array-item-full">${this.escapeHtml(String(item))}</div>`;
});
html += `
<button class="collapse-array-btn" onclick="this.parentElement.style.display='none'; this.parentElement.previousElementSibling.style.display='block';">
Show less
</button>
`;
html += '</div>';
}
html += '</div>';
return html;
}
if (typeof value === 'object' && value !== null) {
return this.formatObjectExpandable(value);
}
return this.escapeHtml(String(value));
}
/**
* NEW: Format objects as expandable content
*/
formatObjectExpandable(obj) {
if (!obj || typeof obj !== 'object') return '';
const entries = Object.entries(obj);
if (entries.length === 0) return '<em>Empty</em>';
if (entries.length <= 3) {
// Simple inline display for small objects
let html = '<div class="simple-object">';
entries.forEach(([key, value]) => {
html += `<div><strong>${key}:</strong> ${this.escapeHtml(String(value))}</div>`;
});
html += '</div>';
return html;
}
// Expandable display for complex objects
let html = '<div class="expandable-object">';
html += `
<div class="object-preview">
<strong>${entries[0][0]}:</strong> ${this.escapeHtml(String(entries[0][1]))}
<button class="expand-object-btn" onclick="this.parentElement.style.display='none'; this.parentElement.nextElementSibling.style.display='block';">
+${entries.length - 1} more properties...
</button>
</div>
<div class="object-full" style="display: none;">
`;
entries.forEach(([key, value]) => {
html += `<div class="object-property"><strong>${key}:</strong> ${this.escapeHtml(String(value))}</div>`;
});
html += `
<button class="collapse-object-btn" onclick="this.parentElement.style.display='none'; this.parentElement.previousElementSibling.style.display='block';">
Show less
</button>
</div>
</div>
`;
return html;
}
formatAttributeLabel(name) {
// Handle provider prefixed attributes
if (name.includes('_')) {
const parts = name.split('_');
if (parts.length >= 2) {
const provider = parts[0];
const attribute = parts.slice(1).join('_');
return `${this.provider}: ${this.formatLabel(attribute)}`;
}
}
return this.formatLabel(name);
}
groupAttributesIntelligently(attributes, nodeType) {
const groups = {};
// Define group configurations
const groupConfigs = {
'DNS Records': {
icon: '🔍',
priority: 'high',
keywords: ['dns', 'record', 'a_record', 'cname', 'mx', 'ns', 'txt', 'ptr'],
providers: ['dns']
},
'Certificate Information': {
icon: '🔒',
priority: 'high',
keywords: ['cert', 'certificate', 'ssl', 'tls', 'issuer', 'validity', 'san'],
providers: ['crtsh']
},
'Network Information': {
icon: '🌐',
priority: 'high',
keywords: ['port', 'service', 'banner', 'asn', 'organization', 'country', 'city'],
providers: ['shodan']
},
'Correlation Data': {
icon: '🔗',
priority: 'medium',
keywords: ['correlation', 'shared', 'common'],
providers: []
}
};
// Initialize groups
Object.entries(groupConfigs).forEach(([name, config]) => {
groups[name] = {
...config,
attributes: []
};
});
// Add a catch-all group
groups['Other Information'] = {
icon: '📋',
priority: 'low',
attributes: []
};
// Classify attributes into groups
attributes.forEach(attr => {
let assigned = false;
// Try to assign to a specific group based on provider or keywords
for (const [groupName, config] of Object.entries(groupConfigs)) {
const matchesProvider = config.providers.includes(attr.provider);
const matchesKeyword = config.keywords.some(keyword =>
attr.name.toLowerCase().includes(keyword) ||
attr.type.toLowerCase().includes(keyword)
);
if (matchesProvider || matchesKeyword) {
groups[groupName].attributes.push(attr);
assigned = true;
break;
}
}
// If not assigned to any specific group, put in "Other"
if (!assigned) {
groups['Other Information'].attributes.push(attr);
}
});
// Remove empty groups
Object.keys(groups).forEach(groupName => {
if (groups[groupName].attributes.length === 0) {
delete groups[groupName];
}
});
return groups;
}
/** /**
* UPDATED: Generate large entity details using unified data model * UPDATED: Generate large entity details using unified data model
*/ */