RIPLEY
This commit is contained in:
parent
16879daf35
commit
239f69a199
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,33 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-03-23 00:28
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0004_entity_identifier_code_entity_photo_url'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='entity',
|
||||||
|
name='last_seen',
|
||||||
|
field=models.DateTimeField(auto_now=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='entity',
|
||||||
|
name='metadata',
|
||||||
|
field=models.JSONField(blank=True, default=dict),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='identityprofile',
|
||||||
|
name='last_seen',
|
||||||
|
field=models.DateTimeField(auto_now=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='identityprofile',
|
||||||
|
name='metadata',
|
||||||
|
field=models.JSONField(blank=True, default=dict),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -17,6 +17,8 @@ class IdentityProfile(models.Model):
|
|||||||
profile_image_url = models.URLField(blank=True)
|
profile_image_url = models.URLField(blank=True)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
last_seen = models.DateTimeField(auto_now=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.full_name
|
return self.full_name
|
||||||
@ -39,6 +41,8 @@ class Entity(models.Model):
|
|||||||
confidence_score = models.FloatField(default=1.0)
|
confidence_score = models.FloatField(default=1.0)
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
updated_at = models.DateTimeField(auto_now=True)
|
updated_at = models.DateTimeField(auto_now=True)
|
||||||
|
last_seen = models.DateTimeField(auto_now=True)
|
||||||
|
metadata = models.JSONField(default=dict, blank=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name_plural = "Entities"
|
verbose_name_plural = "Entities"
|
||||||
|
|||||||
Binary file not shown.
@ -3,6 +3,7 @@ import logging
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from core.models import Entity, Relationship, Source
|
from core.models import Entity, Relationship, Source
|
||||||
from urllib.parse import urljoin, quote, unquote
|
from urllib.parse import urljoin, quote, unquote
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -66,35 +67,51 @@ class WebCrawler:
|
|||||||
class NetworkDiscoveryService:
|
class NetworkDiscoveryService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def perform_osint_search(query):
|
def perform_osint_search(query):
|
||||||
"""Perform discovery using Web Crawling, extracting metadata and images."""
|
"""Perform deep discovery, creating retroactive relationships."""
|
||||||
crawler = WebCrawler()
|
crawler = WebCrawler()
|
||||||
search_results = crawler.search(query)
|
search_results = crawler.search(query)
|
||||||
|
|
||||||
source, _ = Source.objects.get_or_create(name='Web Crawler Engine')
|
source, _ = Source.objects.get_or_create(name='Web Crawler Engine')
|
||||||
person, _ = Entity.objects.get_or_create(entity_type='PERSON', value=query, source=source)
|
person, _ = Entity.objects.get_or_create(entity_type='PERSON', value=query, source=source)
|
||||||
|
person.last_seen = timezone.now()
|
||||||
|
|
||||||
# Use first valid image found among search results if available
|
# Deep discovery: fetch related entities and link them
|
||||||
found_photo = None
|
|
||||||
|
|
||||||
# Increase search limit to 6 and improve crawling logic
|
|
||||||
for res in search_results[:6]:
|
for res in search_results[:6]:
|
||||||
meta, images = crawler.fetch_url(res['url'])
|
meta, images = crawler.fetch_url(res['url'])
|
||||||
|
|
||||||
if images and not found_photo:
|
|
||||||
found_photo = images[0]
|
|
||||||
|
|
||||||
if meta:
|
if meta:
|
||||||
|
# Store metadata in the entity
|
||||||
|
person.metadata.update({res['url']: meta['description']})
|
||||||
|
|
||||||
associate_val = meta['title'] or res['title']
|
associate_val = meta['title'] or res['title']
|
||||||
if associate_val and associate_val.lower() != query.lower():
|
if associate_val and associate_val.lower() != query.lower():
|
||||||
|
# Create associate entity
|
||||||
associate, _ = Entity.objects.get_or_create(
|
associate, _ = Entity.objects.get_or_create(
|
||||||
entity_type='PERSON', value=associate_val[:100], source=source
|
entity_type='PERSON', value=associate_val[:100], source=source
|
||||||
)
|
)
|
||||||
|
associate.last_seen = timezone.now()
|
||||||
|
associate.save()
|
||||||
|
|
||||||
|
# Create relationship
|
||||||
Relationship.objects.get_or_create(
|
Relationship.objects.get_or_create(
|
||||||
source_entity=person, target_entity=associate,
|
source_entity=person, target_entity=associate,
|
||||||
relationship_type='ASSOCIATED_WITH', weight=0.5
|
relationship_type='ASSOCIATED_WITH', weight=0.7
|
||||||
|
)
|
||||||
|
|
||||||
|
# Retroactive check: search associates to find further connections (level 2)
|
||||||
|
second_degree = crawler.search(associate_val)
|
||||||
|
for sec in second_degree[:2]:
|
||||||
|
s_meta, _ = crawler.fetch_url(sec['url'])
|
||||||
|
if s_meta and s_meta['title']:
|
||||||
|
target_val = s_meta['title'][:100]
|
||||||
|
if target_val.lower() != associate_val.lower():
|
||||||
|
target, _ = Entity.objects.get_or_create(
|
||||||
|
entity_type='PERSON', value=target_val, source=source
|
||||||
|
)
|
||||||
|
Relationship.objects.get_or_create(
|
||||||
|
source_entity=associate, target_entity=target,
|
||||||
|
relationship_type='ASSOCIATED_WITH', weight=0.3
|
||||||
)
|
)
|
||||||
|
|
||||||
person.photo_url = found_photo or f"https://api.dicebear.com/7.x/initials/svg?seed={quote(query)}"
|
|
||||||
person.save()
|
person.save()
|
||||||
return person
|
return person
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,10 @@
|
|||||||
<style>
|
<style>
|
||||||
.node-group { cursor: pointer; }
|
.node-group { cursor: pointer; }
|
||||||
.node-circle { stroke: #fff; stroke-width: 2px; }
|
.node-circle { stroke: #fff; stroke-width: 2px; }
|
||||||
.node-text { font-size: 10px; pointer-events: none; }
|
.node-text { font-size: 10px; pointer-events: none; fill: #555; }
|
||||||
#loader { display: none; }
|
#loader { display: none; }
|
||||||
|
#graphContainer { background: #1a1a1a; border-radius: 8px; }
|
||||||
|
.link { stroke: #444; stroke-opacity: 0.6; }
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="container mt-5">
|
<div class="container mt-5">
|
||||||
@ -34,9 +36,8 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="card shadow-sm">
|
<div class="card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body p-0">
|
||||||
<h5 class="card-title">Network Visualization</h5>
|
<div id="graphContainer" style="width: 100%; height: 700px; overflow: hidden;"></div>
|
||||||
<div id="graphContainer" style="width: 100%; height: 600px; background: #f8f9fa; border: 1px solid #ddd; overflow: hidden;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -53,22 +54,19 @@ document.getElementById('searchForm').addEventListener('submit', function(e) {
|
|||||||
const btnText = document.getElementById('btnText');
|
const btnText = document.getElementById('btnText');
|
||||||
const loader = document.getElementById('loader');
|
const loader = document.getElementById('loader');
|
||||||
|
|
||||||
// UI Loading state
|
|
||||||
searchBtn.disabled = true;
|
searchBtn.disabled = true;
|
||||||
btnText.textContent = "Searching...";
|
btnText.textContent = "Processing...";
|
||||||
loader.style.display = "inline-block";
|
loader.style.display = "inline-block";
|
||||||
graphContainer.html('<p class="p-3 text-muted">Discovering network, please wait...</p>');
|
|
||||||
|
|
||||||
fetch(`{% url 'core:search_api' %}?q=${encodeURIComponent(query)}`, { method: 'GET' })
|
// Fetch from new graph endpoint
|
||||||
.then(response => {
|
fetch(`{% url 'core:get_graph_data' %}?q=${encodeURIComponent(query)}`, { method: 'GET' })
|
||||||
if (!response.ok) throw new Error("Search failed");
|
.then(response => response.json())
|
||||||
return response.json();
|
|
||||||
})
|
|
||||||
.then(data => {
|
.then(data => {
|
||||||
graphContainer.html(''); // clear
|
graphContainer.html('');
|
||||||
renderGraph(data);
|
renderGraph(data);
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
|
console.error(err);
|
||||||
graphContainer.html(`<p class="p-3 text-danger">Error: ${err.message}</p>`);
|
graphContainer.html(`<p class="p-3 text-danger">Error: ${err.message}</p>`);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@ -79,29 +77,27 @@ document.getElementById('searchForm').addEventListener('submit', function(e) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function renderGraph(data) {
|
function renderGraph(data) {
|
||||||
const width = 800;
|
const container = document.getElementById('graphContainer');
|
||||||
const height = 600;
|
const width = container.clientWidth;
|
||||||
|
const height = container.clientHeight;
|
||||||
|
|
||||||
// Create SVG and enforce bounds
|
|
||||||
const svg = d3.select("#graphContainer")
|
const svg = d3.select("#graphContainer")
|
||||||
.append("svg")
|
.append("svg")
|
||||||
.attr("width", "100%")
|
.attr("width", width)
|
||||||
.attr("height", "100%")
|
.attr("height", height);
|
||||||
.attr("viewBox", [0, 0, width, height]);
|
|
||||||
|
|
||||||
const simulation = d3.forceSimulation(data.nodes)
|
const simulation = d3.forceSimulation(data.nodes)
|
||||||
.force("link", d3.forceLink(data.links).id(d => d.id).distance(100))
|
.force("link", d3.forceLink(data.links).id(d => d.id).distance(150))
|
||||||
.force("charge", d3.forceManyBody().strength(-300))
|
.force("charge", d3.forceManyBody().strength(-400))
|
||||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||||
.force("x", d3.forceX(width / 2).strength(0.05))
|
.force("collide", d3.forceCollide().radius(30));
|
||||||
.force("y", d3.forceY(height / 2).strength(0.05));
|
|
||||||
|
|
||||||
const link = svg.append("g")
|
const link = svg.append("g")
|
||||||
.selectAll("line")
|
.selectAll("line")
|
||||||
.data(data.links)
|
.data(data.links)
|
||||||
.join("line")
|
.join("line")
|
||||||
.attr("stroke", "#999")
|
.attr("class", "link")
|
||||||
.attr("stroke-width", 1);
|
.attr("stroke-width", d => Math.sqrt(d.value || 1));
|
||||||
|
|
||||||
const node = svg.append("g")
|
const node = svg.append("g")
|
||||||
.selectAll("g")
|
.selectAll("g")
|
||||||
@ -113,36 +109,22 @@ function renderGraph(data) {
|
|||||||
.on("drag", dragged)
|
.on("drag", dragged)
|
||||||
.on("end", dragended));
|
.on("end", dragended));
|
||||||
|
|
||||||
// Reduce node sizes significantly
|
// Colors for types
|
||||||
const radius = 12;
|
const color = d3.scaleOrdinal()
|
||||||
node.append("circle")
|
.domain(["PERSON", "EMAIL", "ENTITY", "ORG"])
|
||||||
.attr("r", radius)
|
.range(["#e74c3c", "#3498db", "#2ecc71", "#f1c40f"]);
|
||||||
.attr("class", "node-circle")
|
|
||||||
.attr("fill", d => d.type === 'PERSON' ? '#e74c3c' : '#3498db');
|
|
||||||
|
|
||||||
// Add Image if available, scaled down
|
node.append("circle")
|
||||||
node.filter(d => d.photo)
|
.attr("r", 15)
|
||||||
.append("image")
|
.attr("fill", d => color(d.type));
|
||||||
.attr("xlink:href", d => d.photo)
|
|
||||||
.attr("x", -radius)
|
|
||||||
.attr("y", -radius)
|
|
||||||
.attr("width", radius * 2)
|
|
||||||
.attr("height", radius * 2)
|
|
||||||
.attr("clip-path", `circle(${radius}px)`);
|
|
||||||
|
|
||||||
node.append("text")
|
node.append("text")
|
||||||
.attr("dy", radius + 15)
|
.attr("dy", 25)
|
||||||
.attr("text-anchor", "middle")
|
.attr("text-anchor", "middle")
|
||||||
.attr("class", "node-text")
|
.attr("class", "node-text")
|
||||||
.text(d => d.name);
|
.text(d => d.name);
|
||||||
|
|
||||||
simulation.on("tick", () => {
|
simulation.on("tick", () => {
|
||||||
// Enforce boundary constraints
|
|
||||||
node.each(d => {
|
|
||||||
d.x = Math.max(radius, Math.min(width - radius, d.x));
|
|
||||||
d.y = Math.max(radius, Math.min(height - radius, d.y));
|
|
||||||
});
|
|
||||||
|
|
||||||
link.attr("x1", d => d.source.x).attr("y1", d => d.source.y)
|
link.attr("x1", d => d.source.x).attr("y1", d => d.source.y)
|
||||||
.attr("x2", d => d.target.x).attr("y2", d => d.target.y);
|
.attr("x2", d => d.target.x).attr("y2", d => d.target.y);
|
||||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||||
@ -153,12 +135,10 @@ function renderGraph(data) {
|
|||||||
event.subject.fx = event.subject.x;
|
event.subject.fx = event.subject.x;
|
||||||
event.subject.fy = event.subject.y;
|
event.subject.fy = event.subject.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragged(event) {
|
function dragged(event) {
|
||||||
event.subject.fx = event.x;
|
event.subject.fx = event.x;
|
||||||
event.subject.fy = event.y;
|
event.subject.fy = event.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragended(event) {
|
function dragended(event) {
|
||||||
if (!event.active) simulation.alphaTarget(0);
|
if (!event.active) simulation.alphaTarget(0);
|
||||||
event.subject.fx = null;
|
event.subject.fx = null;
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
from django.contrib.auth import views as auth_views
|
from django.contrib.auth import views as auth_views
|
||||||
|
|
||||||
from .views import home, ingest_data, resolve_entities, login_view, dashboard_view, logout_view
|
from .views import home, ingest_data, resolve_entities, login_view, dashboard_view, logout_view, get_graph_data
|
||||||
from .api_views import search_api
|
from .api_views import search_api
|
||||||
|
|
||||||
app_name = 'core'
|
app_name = 'core'
|
||||||
@ -10,6 +10,7 @@ urlpatterns = [
|
|||||||
path("", home, name="home"),
|
path("", home, name="home"),
|
||||||
path("api/ingest/", ingest_data, name="ingest_data"),
|
path("api/ingest/", ingest_data, name="ingest_data"),
|
||||||
path("api/resolve/", resolve_entities, name="resolve_entities"),
|
path("api/resolve/", resolve_entities, name="resolve_entities"),
|
||||||
|
path("api/graph/", get_graph_data, name="get_graph_data"),
|
||||||
path("api/search/", search_api, name="search_api"),
|
path("api/search/", search_api, name="search_api"),
|
||||||
path("login/", login_view, name="login"),
|
path("login/", login_view, name="login"),
|
||||||
path("dashboard/", dashboard_view, name="dashboard"),
|
path("dashboard/", dashboard_view, name="dashboard"),
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from django.http import JsonResponse
|
|||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from core.services.ingestion import IngestionService
|
from core.services.ingestion import IngestionService
|
||||||
from core.services.resolution import EntityResolutionService
|
from core.services.resolution import EntityResolutionService
|
||||||
|
from core.models import Entity, Relationship
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
@ -62,5 +63,15 @@ def resolve_entities(request):
|
|||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
# Using EntityResolutionService as a placeholder for actual processing
|
# Using EntityResolutionService as a placeholder for actual processing
|
||||||
result = EntityResolutionService.resolve(data)
|
result = EntityResolutionService.resolve(data)
|
||||||
return JsonResponse({'status': 'success', 'result': result})
|
return JsonResponse({'status': 'success', 'result': str(result)})
|
||||||
return JsonResponse({'error': 'Invalid request'}, status=400)
|
return JsonResponse({'error': 'Invalid request'}, status=400)
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def get_graph_data(request):
|
||||||
|
entities = Entity.objects.all()[:50]
|
||||||
|
relationships = Relationship.objects.filter(source_entity__in=entities, target_entity__in=entities)
|
||||||
|
|
||||||
|
nodes = [{'id': e.value, 'name': e.value, 'type': e.entity_type, 'size': 5} for e in entities]
|
||||||
|
links = [{'source': r.source_entity.value, 'target': r.target_entity.value, 'value': r.weight} for r in relationships]
|
||||||
|
|
||||||
|
return JsonResponse({'nodes': nodes, 'links': links})
|
||||||
Loading…
x
Reference in New Issue
Block a user