diff --git a/app.py b/app.py
index 6d8757d..d8238c4 100644
--- a/app.py
+++ b/app.py
@@ -54,6 +54,14 @@
"timestamp": None,
"active_flows": 0
}
+ENTROPY_PREV = {
+ "timestamp": None,
+ "disk_read_bytes": None,
+ "disk_write_bytes": None,
+ "net_sent_bytes": None,
+ "net_recv_bytes": None,
+ "interrupt_total": None
+}
EXEC_CONTEXT_PREV = {
"timestamp": None,
"irq_totals": {},
@@ -3465,6 +3473,137 @@ def has_token(tokens):
})
return result
+def read_sysctl_int(path, default=0):
+ """Read integer sysctl/proc file value safely."""
+ try:
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
+ raw = f.read().strip()
+ return int(raw or default)
+ except Exception:
+ return int(default)
+
+def read_proc_interrupt_total():
+ """Read total interrupts count from /proc/stat."""
+ try:
+ with open("/proc/stat", "r", encoding="utf-8", errors="ignore") as f:
+ for line in f:
+ if line.startswith("intr "):
+ parts = line.strip().split()
+ if len(parts) >= 2:
+ return int(parts[1])
+ except Exception:
+ return 0
+ return 0
+
+def collect_entropy_cloud_status():
+ """
+ Collect Linux random subsystem entropy status and source activity.
+ This is a best-effort realtime heuristic for UI visualization.
+ """
+ now = time.time()
+ entropy_bits = read_sysctl_int("/proc/sys/kernel/random/entropy_avail", 0)
+ pool_size_bits = read_sysctl_int("/proc/sys/kernel/random/poolsize", 256)
+ read_threshold = read_sysctl_int("/proc/sys/kernel/random/read_wakeup_threshold", 128)
+ write_threshold = read_sysctl_int("/proc/sys/kernel/random/write_wakeup_threshold", 64)
+
+ try:
+ disk = psutil.disk_io_counters()
+ except Exception:
+ disk = None
+ try:
+ net = psutil.net_io_counters()
+ except Exception:
+ net = None
+ intr_total = read_proc_interrupt_total()
+
+ prev_ts = ENTROPY_PREV.get("timestamp")
+ dt = max(now - prev_ts, 0.001) if prev_ts else None
+
+ disk_read_now = int(getattr(disk, "read_bytes", 0) or 0)
+ disk_write_now = int(getattr(disk, "write_bytes", 0) or 0)
+ net_sent_now = int(getattr(net, "bytes_sent", 0) or 0)
+ net_recv_now = int(getattr(net, "bytes_recv", 0) or 0)
+
+ if dt:
+ disk_delta = max(
+ (disk_read_now - int(ENTROPY_PREV.get("disk_read_bytes") or disk_read_now))
+ + (disk_write_now - int(ENTROPY_PREV.get("disk_write_bytes") or disk_write_now)),
+ 0
+ )
+ net_delta = max(
+ (net_sent_now - int(ENTROPY_PREV.get("net_sent_bytes") or net_sent_now))
+ + (net_recv_now - int(ENTROPY_PREV.get("net_recv_bytes") or net_recv_now)),
+ 0
+ )
+ intr_delta = max(intr_total - int(ENTROPY_PREV.get("interrupt_total") or intr_total), 0)
+ else:
+ disk_delta = 0
+ net_delta = 0
+ intr_delta = 0
+
+ ENTROPY_PREV["timestamp"] = now
+ ENTROPY_PREV["disk_read_bytes"] = disk_read_now
+ ENTROPY_PREV["disk_write_bytes"] = disk_write_now
+ ENTROPY_PREV["net_sent_bytes"] = net_sent_now
+ ENTROPY_PREV["net_recv_bytes"] = net_recv_now
+ ENTROPY_PREV["interrupt_total"] = intr_total
+
+ def scale_intensity(rate_value, scale):
+ return int(max(0, min(100, (float(rate_value) / float(scale)) * 100.0)))
+
+ disk_rate = (disk_delta / dt) if dt else 0
+ net_rate = (net_delta / dt) if dt else 0
+ intr_rate = (intr_delta / dt) if dt else 0
+
+ irq_intensity = scale_intensity(intr_rate, 25000)
+ disk_intensity = scale_intensity(disk_rate, 80 * 1024 * 1024)
+ net_intensity = scale_intensity(net_rate, 120 * 1024 * 1024)
+ hwrng_intensity = 68 if entropy_bits > max(read_threshold, 128) else 34
+
+ sources = [
+ {
+ "source": "interrupt timing",
+ "intensity": irq_intensity,
+ "status": "active" if irq_intensity >= 25 else "low"
+ },
+ {
+ "source": "disk IO",
+ "intensity": disk_intensity,
+ "status": "active" if disk_intensity >= 18 else "low"
+ },
+ {
+ "source": "network timing",
+ "intensity": net_intensity,
+ "status": "active" if net_intensity >= 18 else "low"
+ },
+ {
+ "source": "hardware RNG",
+ "intensity": hwrng_intensity,
+ "status": "active" if hwrng_intensity >= 50 else "limited"
+ }
+ ]
+
+ source_avg = int(sum(s["intensity"] for s in sources) / max(len(sources), 1))
+ entropy_pct = max(0.0, min(1.0, float(entropy_bits) / max(float(pool_size_bits), 1.0)))
+ particle_density = max(16, min(84, int(18 + entropy_pct * 42 + source_avg * 0.35)))
+ key_birth_rate = round(0.6 + entropy_pct * 9.4 + source_avg * 0.06, 2)
+
+ crng_state = "ready" if entropy_bits >= max(read_threshold, 128) else "warming"
+ random_state = "stable" if entropy_bits >= max(write_threshold, 64) else "refilling"
+
+ return {
+ "entropy_pool_bits": int(entropy_bits),
+ "entropy_pool_size_bits": int(pool_size_bits),
+ "crng_state": crng_state,
+ "random_subsystem_state": random_state,
+ "particle_density": int(particle_density),
+ "key_birth_rate_est": float(key_birth_rate),
+ "sources": sources,
+ "read_wakeup_threshold": int(read_threshold),
+ "write_wakeup_threshold": int(write_threshold),
+ "mode": "live-heuristic"
+ }
+
def collect_algorithm_requesters(items, kernel_clients):
"""Infer likely requestor objects that trigger algorithm competition."""
algo_map = {"aes": {}, "sha": {}, "chacha20": {}}
@@ -3748,6 +3887,7 @@ def collect_crypto_realtime():
hw_offload=hw_offload,
algorithm_requesters=algorithm_requesters
)
+ entropy_cloud = collect_entropy_cloud_status()
return {
"items": items[:24],
@@ -3762,6 +3902,7 @@ def collect_crypto_realtime():
"algorithm_competitions": algorithm_competitions,
"algorithm_requesters": algorithm_requesters,
"crypto_stage1": crypto_stage1,
+ "entropy_cloud": entropy_cloud,
"crypto_decision_pipeline": crypto_decision_pipelines.get("aes", {}),
"crypto_decision_pipelines": crypto_decision_pipelines,
"source": "live-heuristic-v2",
diff --git a/index.html b/index.html
index 3404d03..5853c5c 100755
--- a/index.html
+++ b/index.html
@@ -100,7 +100,7 @@
Linux Kernel Ring 0 Visualization
-
+
diff --git a/static/js/crypto-belt.js b/static/js/crypto-belt.js
index 5d3deca..0a9377c 100644
--- a/static/js/crypto-belt.js
+++ b/static/js/crypto-belt.js
@@ -578,6 +578,26 @@ class CryptoSubsystemVisualization {
};
}
+ getEntropyPayload(meta) {
+ return meta?.entropy_cloud || {
+ entropy_pool_bits: 256,
+ entropy_pool_size_bits: 256,
+ crng_state: 'ready',
+ random_subsystem_state: 'stable',
+ particle_density: 44,
+ key_birth_rate_est: 6.2,
+ sources: [
+ { source: 'interrupt timing', intensity: 72, status: 'active' },
+ { source: 'disk IO', intensity: 45, status: 'active' },
+ { source: 'network timing', intensity: 38, status: 'active' },
+ { source: 'hardware RNG', intensity: 62, status: 'active' }
+ ],
+ read_wakeup_threshold: 128,
+ write_wakeup_threshold: 64,
+ mode: 'mock'
+ };
+ }
+
drawAlgorithmCompetition(layer, meta, width) {
const comp = this.getCompetitionPayload(meta);
const request = String(comp.request || this.selectedCompetitionAlgorithm || 'AES').toUpperCase();
@@ -922,17 +942,13 @@ class CryptoSubsystemVisualization {
const requesters = Array.isArray(pipeline?.requesters) ? pipeline.requesters : [];
const topRequester = requesters.length ? requesters[0] : null;
- const cardW = Math.max(320, Math.min(430, Math.floor(width * 0.27)));
- const cardH = 214;
- const cardX = Math.floor(width * 0.43);
- // Align card vertically with the Stage-1 left stack (starts at KERNEL CRYPTO CLIENTS).
- const stageGap = 12;
- const stageClientsH = 134;
- const stageQueueH = 86;
- const stageOffloadH = 116;
- const stageTotalH = stageClientsH + stageQueueH + stageOffloadH + (stageGap * 2);
- const stageBaseY = Math.max(180, height - stageTotalH - 20);
- const cardY = stageBaseY;
+ const rightColumnX = Math.floor(width * 0.73);
+ const cardX = Math.floor(width * 0.41) + 10;
+ const maxSafeW = Math.max(300, rightColumnX - cardX - 16);
+ const cardW = Math.max(330, Math.min(Math.min(470, Math.floor(width * 0.3)), maxSafeW));
+ const cardH = 226;
+ const decisionPanelY = Math.max(450, Math.floor(height * 0.52));
+ const cardY = decisionPanelY;
const card = layer.append('g').attr('class', 'crypto-material-card');
card.append('rect')
@@ -1052,7 +1068,7 @@ class CryptoSubsystemVisualization {
card.append('text')
.attr('x', cloudX + 8)
- .attr('y', cardY + cardH - 12)
+ .attr('y', cardY + cardH - 14)
.style('font-family', 'Share Tech Mono, monospace')
.style('font-size', '8.5px')
.style('fill', '#8f9eb3')
@@ -1061,7 +1077,7 @@ class CryptoSubsystemVisualization {
if (this.selectedImplementationClassFilter) {
card.append('text')
.attr('x', leftX)
- .attr('y', cardY + cardH - 12)
+ .attr('y', cardY + cardH - 14)
.style('font-family', 'Share Tech Mono, monospace')
.style('font-size', '8.5px')
.style('fill', '#c8d6ea')
@@ -1069,6 +1085,182 @@ class CryptoSubsystemVisualization {
}
}
+ drawEntropyCloud(layer, meta, width, height) {
+ const entropy = this.getEntropyPayload(meta);
+ const sources = Array.isArray(entropy.sources) ? entropy.sources.slice(0, 4) : [];
+ const rightColumnX = Math.floor(width * 0.73);
+ const panelX = Math.floor(width * 0.41) + 10;
+ const maxSafeW = Math.max(300, rightColumnX - panelX - 16);
+ const panelW = Math.max(330, Math.min(Math.min(470, Math.floor(width * 0.3)), maxSafeW));
+ const panelH = 238;
+ const decisionPanelY = Math.max(450, Math.floor(height * 0.52));
+ const materialCardH = 226;
+ const panelY = Math.min(height - panelH - 22, decisionPanelY + materialCardH + 16);
+ const panel = layer.append('g').attr('class', 'crypto-entropy-cloud');
+
+ panel.append('rect')
+ .attr('x', panelX)
+ .attr('y', panelY)
+ .attr('width', panelW)
+ .attr('height', panelH)
+ .attr('rx', 9)
+ .style('fill', 'rgba(7, 10, 15, 0.9)')
+ .style('stroke', 'rgba(162, 176, 198, 0.32)')
+ .style('stroke-width', 1);
+
+ panel.append('text')
+ .attr('x', panelX + 14)
+ .attr('y', panelY + 22)
+ .style('font-family', 'Share Tech Mono, monospace')
+ .style('font-size', '11px')
+ .style('letter-spacing', '0.45px')
+ .style('fill', '#d7ddea')
+ .text('KERNEL ENTROPY CLOUD');
+
+ const poolBits = Number(entropy.entropy_pool_bits || 0);
+ const poolSizeBits = Math.max(Number(entropy.entropy_pool_size_bits || 256), 1);
+ const poolPct = Math.max(0, Math.min(100, (poolBits / poolSizeBits) * 100));
+ panel.append('text')
+ .attr('x', panelX + 14)
+ .attr('y', panelY + 42)
+ .style('font-family', 'Share Tech Mono, monospace')
+ .style('font-size', '9.5px')
+ .style('fill', '#a8b5c8')
+ .text(`entropy pool: ${poolBits}/${poolSizeBits} bits (${poolPct.toFixed(0)}%)`);
+ panel.append('text')
+ .attr('x', panelX + 14)
+ .attr('y', panelY + 58)
+ .style('font-family', 'Share Tech Mono, monospace')
+ .style('font-size', '9.5px')
+ .style('fill', '#a8b5c8')
+ .text(`CRNG (ChaCha20): ${String(entropy.crng_state || 'unknown').toUpperCase()}`);
+ panel.append('text')
+ .attr('x', panelX + 14)
+ .attr('y', panelY + 74)
+ .style('font-family', 'Share Tech Mono, monospace')
+ .style('font-size', '9.5px')
+ .style('fill', '#a8b5c8')
+ .text(`random subsystem: ${String(entropy.random_subsystem_state || 'unknown').toUpperCase()}`);
+
+ const cloudX = panelX + 12;
+ const cloudY = panelY + 86;
+ // Keep particle viewport compact so right-side source metrics fit comfortably.
+ const cloudW = Math.max(130, panelW - 220);
+ const cloudH = panelH - 100;
+ panel.append('rect')
+ .attr('x', cloudX)
+ .attr('y', cloudY)
+ .attr('width', cloudW)
+ .attr('height', cloudH)
+ .attr('rx', 7)
+ .style('fill', 'rgba(11, 16, 22, 0.7)')
+ .style('stroke', 'rgba(108, 120, 139, 0.24)')
+ .style('stroke-width', 0.8);
+
+ const particleCount = Math.max(12, Math.min(90, Number(entropy.particle_density || 32)));
+ for (let i = 0; i < particleCount; i += 1) {
+ const h = this.hashText(`entropy-${i}`);
+ const px = cloudX + 10 + (h % Math.max(12, cloudW - 20));
+ const py = cloudY + 10 + ((Math.floor(h / 9)) % Math.max(12, cloudH - 20));
+ const phase = (this.activeAnimationTick * 0.55) + (i * 0.37);
+ const pulse = 0.45 + 0.55 * ((Math.sin(phase) + 1) / 2);
+ const radius = 1.3 + ((h % 17) / 18) * 2.1 + pulse * 1.25;
+ const alpha = 0.28 + pulse * 0.64;
+ const hue = i % 7 === 0 ? '#80dbe8' : '#6eb1d5';
+ panel.append('circle')
+ .attr('cx', px)
+ .attr('cy', py)
+ .attr('r', radius)
+ .style('fill', hue)
+ .style('opacity', Math.min(alpha, 0.86));
+ }
+
+ const keyRate = Number(entropy.key_birth_rate_est || 0);
+ const keyNodes = Math.max(1, Math.min(5, Math.round(keyRate / 2.2)));
+ const keyBaseX = panelX + panelW - 110;
+ const keyBaseY = panelY + 112;
+ panel.append('text')
+ .attr('x', keyBaseX)
+ .attr('y', keyBaseY - 12)
+ .style('font-family', 'Share Tech Mono, monospace')
+ .style('font-size', '9px')
+ .style('fill', '#99a8bd')
+ .text(`key births/s est: ${keyRate.toFixed(2)}`);
+
+ for (let i = 0; i < keyNodes; i += 1) {
+ const x = keyBaseX + (i % 3) * 20;
+ const y = keyBaseY + Math.floor(i / 3) * 18;
+ panel.append('rect')
+ .attr('x', x)
+ .attr('y', y)
+ .attr('width', 8)
+ .attr('height', 8)
+ .attr('transform', `rotate(45, ${x + 4}, ${y + 4})`)
+ .style('fill', '#9de8ff')
+ .style('opacity', 0.84);
+ }
+
+ panel.append('line')
+ .attr('x1', cloudX + cloudW + 4)
+ .attr('y1', cloudY + Math.floor(cloudH / 2))
+ .attr('x2', keyBaseX - 6)
+ .attr('y2', keyBaseY + 2)
+ .style('stroke', '#7fc4e8')
+ .style('stroke-width', 1)
+ .style('stroke-opacity', 0.7);
+
+ const srcX = panelX + panelW - 158;
+ const srcY = panelY + 152;
+ const sourceBarX = srcX + 70;
+ const sourceBarW = Math.max(64, Math.min(76, panelX + panelW - sourceBarX - 8));
+ panel.append('text')
+ .attr('x', srcX)
+ .attr('y', srcY)
+ .style('font-family', 'Share Tech Mono, monospace')
+ .style('font-size', '9px')
+ .style('fill', '#a2b0c4')
+ .text('entropy sources');
+
+ sources.forEach((item, idx) => {
+ const y = srcY + 15 + idx * 16;
+ const intensity = Math.max(0, Math.min(100, Number(item.intensity || 0)));
+ const status = String(item.status || 'low').toLowerCase();
+ const barW = Math.round((intensity / 100) * sourceBarW);
+ const color = status === 'active' ? '#8ff0ff' : (status === 'limited' ? '#ffd18d' : '#8798ad');
+ panel.append('text')
+ .attr('x', srcX)
+ .attr('y', y)
+ .style('font-family', 'Share Tech Mono, monospace')
+ .style('font-size', '8.5px')
+ .style('fill', '#95a5bb')
+ .text(String(item.source || 'source'));
+ panel.append('rect')
+ .attr('x', sourceBarX)
+ .attr('y', y - 8)
+ .attr('width', sourceBarW)
+ .attr('height', 5)
+ .attr('rx', 2)
+ .style('fill', 'rgba(35, 42, 52, 0.9)');
+ panel.append('rect')
+ .attr('x', sourceBarX)
+ .attr('y', y - 8)
+ .attr('width', barW)
+ .attr('height', 5)
+ .attr('rx', 2)
+ .style('fill', color)
+ .style('opacity', 0.88);
+ });
+
+ panel.append('text')
+ .attr('x', panelX + panelW - 10)
+ .attr('y', panelY + panelH - 8)
+ .attr('text-anchor', 'end')
+ .style('font-family', 'Share Tech Mono, monospace')
+ .style('font-size', '8px')
+ .style('fill', '#718199')
+ .text(`mode: ${String(entropy.mode || 'live-heuristic')}`);
+ }
+
drawStage1Panels(layer, meta, width, height) {
const stage = meta?.crypto_stage1 || {};
const clients = Array.isArray(stage.kernel_clients) ? stage.kernel_clients.slice(0, 6) : [];
@@ -1396,6 +1588,7 @@ class CryptoSubsystemVisualization {
const layer = this.svg.append('g').attr('class', 'crypto-flow-layer');
this.drawGrid(layer, width, height);
this.drawProtocolLegend(layer);
+ this.drawEntropyCloud(layer, payload?.meta || {}, width, height);
this.drawAlgorithmCompetition(layer, payload?.meta || {}, width);
this.drawDecisionPipeline(layer, payload?.meta || {}, width, height);
this.drawAlgorithmMaterialCard(layer, payload?.meta || {}, width, height);
@@ -1724,6 +1917,23 @@ class CryptoSubsystemVisualization {
{ name: 'AF_ALG', kind: 'kernel-client', score: 1 }
]
},
+ entropy_cloud: {
+ entropy_pool_bits: 238,
+ entropy_pool_size_bits: 256,
+ crng_state: 'ready',
+ random_subsystem_state: 'stable',
+ particle_density: 52,
+ key_birth_rate_est: 7.4,
+ sources: [
+ { source: 'interrupt timing', intensity: 76, status: 'active' },
+ { source: 'disk IO', intensity: 42, status: 'active' },
+ { source: 'network timing', intensity: 38, status: 'active' },
+ { source: 'hardware RNG', intensity: 64, status: 'active' }
+ ],
+ read_wakeup_threshold: 128,
+ write_wakeup_threshold: 64,
+ mode: 'mock'
+ },
source: 'mock'
}
};
diff --git a/templates/linux-crypto-subsystem.html b/templates/linux-crypto-subsystem.html
index d7997ed..9ea400f 100644
--- a/templates/linux-crypto-subsystem.html
+++ b/templates/linux-crypto-subsystem.html
@@ -135,7 +135,7 @@ Linux Crypto Kernel Visualization
-
+