Skip to content

feat: implement advanced dashboard enhancements and devops automation - #2

Open
khulnasoft-bot wants to merge 4 commits into
masterfrom
3.0.0
Open

feat: implement advanced dashboard enhancements and devops automation#2
khulnasoft-bot wants to merge 4 commits into
masterfrom
3.0.0

Conversation

@khulnasoft-bot

@khulnasoft-bot khulnasoft-bot commented Jan 15, 2026

Copy link
Copy Markdown
  • Add 3D Globe visualization using Three.js and Globe.gl
  • Implement 2D/3D view toggle for attack monitoring
  • Add attack classification and audio-HUD feedback
  • Integrate IP reputation and enhanced source/destination tracking
  • Setup CI/CD pipeline via GitHub Actions
  • Containerize application with Docker and Docker Compose
  • Add Makefile for streamlined local development and maintenance

Summary by Sourcery

Enhance attack monitoring with interactive 3D visualization, threat intelligence feedback, historical playback, and automated containerized delivery.

New Features:

  • Add a 3D globe view with interactive attack arcs, attacker points, and contextual insights alongside the existing 2D map.
  • Enable attack classification with severity-based visual indicators, audio alerts, and enhanced live-feed information.
  • Add time-machine playback for reviewing cached attacks and replaying historical map activity.

Bug Fixes:

  • Make backend service endpoints configurable through environment variables and use broadly compatible timezone-aware UTC handling.

Enhancements:

  • Improve attack source and destination tracking with IP reputation and classification data in dashboard tables.
  • Integrate 2D/3D view controls and threat-focused visual feedback into the dashboard.

Build:

  • Add Docker and Docker Compose configurations for running the attack map, data server, Redis, and Elasticsearch services.
  • Streamline Python tooling commands and dependency invocation through the Makefile.

CI:

  • Add GitHub Actions workflows for linting, testing, integrity validation, Docker Compose validation, image builds, registry publishing, and image signing.

Tests:

  • Update data-server timing tests for the revised UTC datetime handling.

Summary by CodeRabbit

  • New Features

    • 3D globe visualization, playback/time-travel controls, expanded dashboard UI (playback bar, settings modal)
    • Audio alert system with multiple sound profiles, previews, and master volume
    • Attack classification with severity badges, visual alerts, and enrichment of live feed
  • Refactor

    • Servers now read configuration from environment variables; timezone handling made UTC-aware
  • Chores

    • CI/CD workflows, Dockerfile, and docker-compose added for building and running the app
  • Tests

    • Minor test cleanup around timezone mocking

- Add 3D Globe visualization using Three.js and Globe.gl
- Implement 2D/3D view toggle for attack monitoring
- Add attack classification and audio-HUD feedback
- Integrate IP reputation and enhanced source/destination tracking
- Setup CI/CD pipeline via GitHub Actions
- Containerize application with Docker and Docker Compose
- Add Makefile for streamlined local development and maintenance
@coderabbitai

coderabbitai Bot commented Jan 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 58 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d02d8852-2a8a-4e91-b07f-027636078678

📥 Commits

Reviewing files that changed from the base of the PR and between a1b42d9 and 7b9172b.

📒 Files selected for processing (1)
  • static/globe-view.js

Walkthrough

Adds CI and Docker publish workflows, Dockerfile and compose; moves server configs to environment variables and bumps versions; introduces AudioHUD, AttackClassifier, GlobeView, playback/time-travel, UI/CSS overhaul, and related client-side integration changes.

Changes

Cohort / File(s) Summary
CI / Publishing Workflows
.github/workflows/ci.yml, .github/workflows/docker-publish.yml
Adds CI (lint-and-test, build-docker) and Docker publish workflows with buildx, caching, metadata extraction, and optional cosign signing.
Container & Compose
Dockerfile, docker-compose.yml
Adds Dockerfile for Python app image and docker-compose orchestration for attack-map-server, data-server, Redis, and Elasticsearch with environment wiring and network.
Server config & Make
AttackMapServer.py, DataServer.py, Makefile
Switches hardcoded Redis/ES/port values to env vars (MAP_REDIS_URL, MAP_WEB_PORT, MAP_ES_URL, MAP_REDIS_HOST, CYBERPOT_ATTACKMAP_TEXT), bumps version strings, makes datetime timezone-aware, and adjusts Makefile to use python -m invocations for flake8/pytest.
Frontend — Audio & Intelligence
static/audio-hud.js, static/intelligence.js
Adds AudioHUD (Web Audio API, localStorage, master volume, multiple sounds) and AttackClassifier (pattern matching, severity tagging); exposes window.audioHud and window.attackClassifier.
Frontend — 3D Globe & Dashboard
static/globe-view.js, static/dashboard.js, static/map.js
Adds GlobeView (Globe.gl/Three.js integration), playback/time-travel system, classification integration, audio sync, 2D/3D toggle, playback-aware restoration (new/updated method signatures), and visual alert triggers.
Frontend — Markup & Styles
static/index.html, static/index.css
Major HTML restructure: globe container, playback bar, settings modal, expanded tabs; large CSS additions for globe, playback UI, protocol badges, animations, notifications; some duplicate/merge-artifact sections present.
Tests
tests/test_DataServer.py
Removes mocking of a UTC attribute on datetime mocks; minor test cleanup.

Sequence Diagrams

sequenceDiagram
    participant Attacker
    participant WebSocket as Real-time Feed
    participant MapJS as map.js
    participant Classifier as AttackClassifier
    participant Globe as GlobeView
    participant AudioHUD as AudioHUD
    participant Dashboard as Dashboard UI

    Attacker->>WebSocket: Attack packet
    WebSocket->>MapJS: onTraffic(msg)
    MapJS->>Classifier: classify(msg)
    Classifier-->>MapJS: {id, severity, tags}
    MapJS->>MapJS: Render marker on map
    MapJS->>Globe: addAttack(msg)
    Globe->>Globe: Map to arc & point
    Globe->>Dashboard: Update visualization
    MapJS->>AudioHUD: trigger(msg)
    AudioHUD->>AudioHUD: Play sound based on selection/severity
    MapJS->>Dashboard: Update live feed with classification
    Dashboard->>Dashboard: Add badge & visual alert
Loading
sequenceDiagram
    participant User
    participant Dashboard as Dashboard UI
    participant PlaybackSystem as Playback Engine
    participant MapJS as map.js
    participant Globe as GlobeView
    participant Timeline as Timeline Data

    User->>Dashboard: Click timeline / enter playback
    Dashboard->>PlaybackSystem: enterPlaybackMode()
    PlaybackSystem->>Timeline: Load historical events
    PlaybackSystem->>Dashboard: Show playback bar
    User->>Dashboard: Scrub slider
    Dashboard->>PlaybackSystem: updatePlaybackView(index)
    PlaybackSystem->>MapJS: processRestoredAttack(event, isPlayback=true)
    MapJS->>MapJS: Restore marker (skip aggregation where applicable)
    MapJS->>Globe: addRestoredAttack(event)
    Globe->>Globe: Render historical arc/point
    User->>Dashboard: Exit playback
    Dashboard->>PlaybackSystem: exitPlaybackMode()
    PlaybackSystem->>Dashboard: Return to live feed
Loading
sequenceDiagram
    participant GitHub as GitHub Actions
    participant Lint as Lint & Test
    participant Docker as Docker Build
    participant Registry as Image Cache

    GitHub->>Lint: Trigger on push/PR or manual
    Lint->>Lint: checkout, setup Python, python -m flake8, python -m pytest, docker compose config
    Lint-->>GitHub: Status: pass/fail
    alt On push to main/master (if lint passes)
        GitHub->>Docker: build-docker job
        Docker->>Registry: use cache-from / cache-to
        Docker->>Docker: Build image (cyberpot-attack-map:latest)
        Docker->>Docker: load image locally
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A hop, a build, a globe that spins so bright,

Beeps and badges glow through dark and light,
Playback rewinds the tales of cyber fights,
Docker hums, CI guards the night,
A rabbit cheers—our map now takes to flight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: it covers both dashboard enhancements (3D globe, audio-HUD, attack classification) and devops automation (CI/CD pipeline, Docker, Docker Compose).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 3.0.0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
static/dashboard.js (3)

329-349: Playback UI: handle empty cache + wire prev/next + null-guards + consider throttling slider

  • enterPlaybackMode() sets slider.max = this.playbackHistory.length - 1 (Line 3242) which becomes -1 when the cache is empty.
  • prevBtn/nextBtn are fetched (Line 3208-3209) but never used.
  • exitBtn.onclick = ... (Line 3213) can throw if the element is missing even though slider/playbackBar exist.
  • slider.oninput will call updatePlaybackView() on every scrub event (Line 3215-3218); if processRestoredAttack is heavy, this will jank—consider rAF-throttling/debouncing.
Proposed fix (empty-cache guard + basic prev/next wiring + safer element checks)
 initPlaybackSystem() {
     const slider = document.getElementById('playback-slider');
     const playbackBar = document.getElementById('playback-bar');
     const exitBtn = document.getElementById('playback-exit');
     const prevBtn = document.getElementById('playback-prev');
     const nextBtn = document.getElementById('playback-next');

     if (!slider || !playbackBar) return;

-    exitBtn.onclick = () => this.exitPlaybackMode();
+    if (exitBtn) exitBtn.onclick = () => this.exitPlaybackMode();

+    if (prevBtn) prevBtn.onclick = () => {
+        this.playbackIndex = Math.max(0, this.playbackIndex - 1);
+        slider.value = String(this.playbackIndex);
+        this.updatePlaybackView();
+    };
+    if (nextBtn) nextBtn.onclick = () => {
+        this.playbackIndex = Math.min(this.playbackHistory.length - 1, this.playbackIndex + 1);
+        slider.value = String(this.playbackIndex);
+        this.updatePlaybackView();
+    };

     slider.oninput = (e) => {
         this.playbackIndex = parseInt(e.target.value);
         this.updatePlaybackView();
     };
@@
 async enterPlaybackMode() {
     if (this.playbackMode) return;

     console.log('[PLAYBACK] Entering Time Machine mode...');
     this.playbackMode = true;
     this.playbackHistory = await this.attackCache.getStoredEvents();
     this.playbackHistory.sort((a, b) => a.timestamp - b.timestamp);

+    if (this.playbackHistory.length === 0) {
+        this.playbackMode = false;
+        this.showNotification('No cached events available for Time Machine mode yet.', 'info', 'playback');
+        return;
+    }

     const playbackBar = document.getElementById('playback-bar');
     const slider = document.getElementById('playback-slider');

     if (playbackBar && slider) {
         playbackBar.classList.remove('hidden');
         slider.max = this.playbackHistory.length - 1;
         slider.value = this.playbackHistory.length - 1;
         this.playbackIndex = this.playbackHistory.length - 1;
         this.updatePlaybackView();
     }

     this.showNotification('Entered Time Machine mode. Live feed paused.', 'info', 'playback');
 }

Also applies to: 3204-3280


2136-2155: AudioHUD integration: use setSound() method instead of direct property assignment, and sync sound dropdown with audioHud

applySettings() sets window.audioHud.selectedSound directly (line 2152-2153) but setSound() method exists and should be used instead. More critically, the alert-sound change handler (lines 2307-2317) updates settings and localStorage but never calls applySettings() or syncs with audioHud, causing drift between the dropdown selection and audio playback.

Proposed fix
 applySettings() {
   // Apply sound alert toggle visibility - use actual settings value, not just checkbox state
   const soundOptions = document.getElementById('sound-options');
   const soundAlerts = document.getElementById('sound-alerts');

   if (soundOptions && soundAlerts) {
     // Use the stored setting value to determine visibility
     soundOptions.style.display = this.settings.soundAlerts ? 'block' : 'none';
     // Ensure checkbox matches the setting
     soundAlerts.checked = this.settings.soundAlerts;
   }

   // Feature Integration: Notify audioHud of current settings
-  if (window.audioHud) {
-      window.audioHud.setEnabled(this.settings.soundAlerts);
-      if (this.settings.alertSound) {
-          window.audioHud.selectedSound = this.settings.alertSound;
-      }
-  }
+  const hud = window.audioHud;
+  if (hud) {
+      hud.setEnabled?.(this.settings.soundAlerts);
+      if (this.settings.alertSound && typeof hud.setSound === 'function') {
+          hud.setSound(this.settings.alertSound);
+      }
+  }
 }
 // Add event listener for alert sound dropdown
 const alertSound = document.getElementById('alert-sound');
 if (alertSound) {
     alertSound.addEventListener('change', () => {
         // Update the settings object immediately
         this.settings.alertSound = alertSound.value;
         // Save to localStorage
         localStorage.setItem('attack-map-settings', JSON.stringify(this.settings));
+        // Sync with audioHud immediately
+        if (window.audioHud?.setSound) {
+            window.audioHud.setSound(alertSound.value);
+        }
         // Show feedback notification
         this.showNotification(
             `Alert sound changed to ${alertSound.options[alertSound.selectedIndex].text}`,
             'success',
             'sound'
         );
     });
 }

2937-2990: Playback mode not enforced in addAttackEvent + missing error handling for classifier + severity case sensitivity

The addAttackEvent() method lacks a guard for playbackMode and will continue to update the history, timeline, heatmap, and trigger sounds/alerts even when playback is active. The notification "Live feed paused" (line 3248) is misleading since new events are still being processed into the UI.

The classifier integration at line 2940 is not wrapped in try/catch; any exception from window.attackClassifier.classify() will break event ingestion entirely.

The severity passed to triggerVisualAlert() (line 2971) is not normalized. The method checks ['critical', 'high'].includes(severity) (line 3784) with case-sensitive comparison, so values like 'Critical' or 'HIGH' will silently fail to trigger the alert.

Proposed fix (playback guard + resilient classification + severity normalization)
 addAttackEvent(event) {
+    // If we're in playback mode, do not mutate live UI state
+    if (this.playbackMode) return;
+
-    // Intelligence Integration: Classify the attack
-    if (window.attackClassifier) {
-        event.classification = window.attackClassifier.classify(event);
-    }

     console.log('[DEBUG] Dashboard received attack event:', event);

     event.timestamp = Date.now();
     this.attackHistory.push(event);

     // Store in cache if initialized and not currently restoring
     if (this.cacheInitialized && !this.restoringFromCache) {
         this.attackCache.storeEvent(event).catch(error => {
             console.warn('[CACHE] Failed to store event:', error);
         });
     }

     // Process attack for honeypot tracking if honeypot field is present
     if (event.honeypot) {
         this.trackHoneypotAttack(event.honeypot, event.timestamp);
     }

     // Try to initialize audio context if not already done (aggressive approach)
     if (!this.audioInitialized) {
         console.log('[SOUND] Attempting audio initialization on attack event');
         this.initializeAudioContext();
     }

     // Play sound alert for new attack
     this.playAlertSound();

     // Feature Integration: Visual Alerts (Phase 3)
+    // Intelligence Integration: Classify the attack (never break ingestion)
+    if (window.attackClassifier?.classify) {
+        try {
+            event.classification = window.attackClassifier.classify(event);
+        } catch (e) {
+            console.warn('[INTEL] Classification failed:', e);
+        }
+    }
-    if (event.classification) {
-        this.triggerVisualAlert(event.classification.severity);
-    }
+    const severity = event.classification?.severity?.toLowerCase?.();
+    if (severity && ['critical', 'high'].includes(severity)) {
+        this.triggerVisualAlert(severity);
+    }
🤖 Fix all issues with AI agents
In `@docker-compose.yml`:
- Around line 37-44: Update the elasticsearch service: change the image tag from
docker.elastic.co/elasticsearch/elasticsearch:8.18.1 to 8.19.9 (service name:
elasticsearch), stop disabling built-in security by removing or setting
xpack.security.enabled to true and supply a development password (e.g.,
ELASTIC_PASSWORD) or document that the instance is intentionally unsecured and
restricted to localhost, and add persistent storage by declaring and mounting a
volume for /usr/share/elasticsearch/data (plus add a top-level named volume
entry) so indexed data survives container recreation.

In `@static/audio-hud.js`:
- Around line 57-74: playCriticalAlert() currently uses this.context directly
and can throw if audio isn't ready; add an initialization guard at the start of
playCriticalAlert (check this.enabled and this.initialized and that this.context
is non-null) and return early if not initialized (or call the existing
init/initialize method if appropriate), ensuring the method never dereferences
this.context when audio is not ready; also update any callers like trigger() to
respect the same guard if they invoke playCriticalAlert.
- Around line 77-93: playRetro() lacks the same initialization/enabled guard
used in playBeep() (and playCriticalAlert()), so add the same early return check
(e.g. verify this.enabled and that the audio context/initialized flag is
present) at the top of playRetro() and only proceed to create oscillators/gains
when the instance is initialized and enabled; mirror the exact guard logic from
playBeep() to ensure consistent behavior across playRetro(),
playCriticalAlert(), and playBeep().

In `@static/dashboard.js`:
- Around line 3779-3806: In triggerVisualAlert, the overview tab is hardcoded to
use 'pulse-high' instead of the computed pulseClass; change the dashTab handling
to add and later remove pulseClass (not the literal 'pulse-high') so the tab
pulse matches the navbar pulseClass, referencing the existing pulseClass and
dashTab symbols and keeping the existing timeouts.

In `@static/globe-view.js`:
- Around line 161-178: The showInsightOverlay function currently assigns
untrusted markup to content.innerHTML which allows XSS; change it to build DOM
nodes safely: for each Object.entries(data) create a container element (class
"insight-item"), create two child elements (classes "insight-label" and
"insight-value"), set their textContent to `${key}:` and `val` respectively,
append them to the container, then append each container to the element returned
by document.getElementById('insight-content') after clearing existing children;
keep the existing header.textContent, overlay.classList.remove and opacity
changes.

In `@static/index.css`:
- Around line 2970-3133: The playback bar (.playback-bar) and globe overlay
(.globe-insight-overlay) use hard-coded offsets (bottom: 350px; right: 200px)
which break with resizable/collapsible panels—replace those fixed values with
CSS variables (e.g., bottom: calc(var(--bottom-panel-height, 350px) + 0px);
right: calc(var(--side-panel-width, 200px) + 0px)) so positioning is driven by
--bottom-panel-height and --side-panel-width, and update any related
z-index/transitions to remain unchanged; also add default fallback values in the
var() calls and ensure the UI JS (panel resize/collapse handlers) sets
document.documentElement.style.setProperty('--bottom-panel-height', 'Xpx') and
'--side-panel-width' accordingly whenever panels resize or toggle (affecting
both .playback-bar and .globe-insight-overlay).

In `@static/index.html`:
- Around line 7-8: Update the page Content-Security-Policy meta by adding
worker-src 'blob:' to allow Globe.gl's Blob-based workers, tighten connect-src
from the broad "wss: ws:" to the specific WebSocket origin used by your app (the
endpoint referenced in map.js at the connection logic around the WS_HOST usage)
or a build-time/whitelisted host, and add frame-ancestors 'none' and base-uri
'self' to the CSP string; modify the content value in the meta tag (the
attribute containing default-src... connect-src...) to include these directives
while preserving existing allowed sources (script-src, style-src, img-src,
etc.).

In `@static/intelligence.js`:
- Around line 58-64: There is a duplicate pattern object with id 'brute_force'
that is unreachable because classify() uses first-match; merge the expanded
protocol list (add 'VNC' and 'RLOGIN') into the existing 'brute_force' pattern
definition (the earlier object with id 'brute_force' and criteria function) and
then delete the later duplicate pattern object so only one 'brute_force' entry
remains.
🧹 Nitpick comments (24)
AttackMapServer.py (2)

14-18: Consider adding validation for MAP_WEB_PORT environment variable.

If MAP_WEB_PORT contains a non-numeric value, int() will raise a ValueError and crash the server on startup. Consider wrapping with a try-except for graceful fallback.

🔧 Suggested defensive parsing
 import os

 # Configuration
 redis_url = os.getenv('MAP_REDIS_URL', 'redis://127.0.0.1:6379')
-web_port = int(os.getenv('MAP_WEB_PORT', '64299'))
+try:
+    web_port = int(os.getenv('MAP_WEB_PORT', '64299'))
+except ValueError:
+    web_port = 64299
+    print(f"[!] Invalid MAP_WEB_PORT value, using default: {web_port}")

50-51: Bare except clause hides specific errors.

Consider catching specific exceptions (e.g., Exception) and logging the error type for better debugging.

🔧 Suggested improvement
-                    except:
-                        print("Something went wrong while sending JSON data.")
+                    except Exception as e:
+                        print(f"Error sending JSON data: {type(e).__name__}")
Dockerfile (2)

20-21: Consider adding a .dockerignore file.

COPY . . will include all files including .git/, __pycache__/, local config files, and potentially sensitive data. A .dockerignore file improves build performance and security.

Example .dockerignore:

.git
.gitignore
__pycache__
*.pyc
*.pyo
.env
.env.*
docker-compose*.yml
Makefile
README.md
tests/

26-27: Consider running as a non-root user for improved security.

The container currently runs as root. Adding a non-root user reduces the attack surface if the application is compromised.

🔒 Suggested improvement
 # Copy the rest of the application code
 COPY . .

+# Create non-root user
+RUN useradd --create-home --shell /bin/bash appuser
+USER appuser

 # Expose the web server port
 EXPOSE 64299
DataServer.py (4)

10-13: Module-level Elasticsearch initialization may fail silently at import time.

The Elasticsearch client is initialized immediately when the module loads. If MAP_ES_URL is malformed, this could cause import failures. Consider deferring initialization or wrapping in a factory function.

🔧 Suggested approach
 # Configuration
 es_url = os.getenv('MAP_ES_URL', 'http://127.0.0.1:9200')
-es = Elasticsearch(es_url)
+es = None
+
+def get_es_client():
+    global es
+    if es is None:
+        es = Elasticsearch(es_url)
+    return es

Then use get_es_client() where es is currently used directly.


442-455: Fragile error detection via string matching.

Detecting Redis vs Elasticsearch errors by checking substrings in error messages (e.g., "6379", "redis", "elastic") is brittle. Consider catching specific exception types instead.

🔧 Suggested approach
             except Exception as e:
-                error_type = type(e).__name__
-                error_msg = str(e)
-                
-                # Check for Redis errors
-                if "6379" in error_msg or "Redis" in error_msg or "redis" in error_msg.lower():
+                from redis.exceptions import RedisError
+                from elasticsearch.exceptions import ElasticsearchException
+                
+                if isinstance(e, RedisError):
                     if not was_disconnected_redis:
-                        print(f"[ ] Connection lost to Redis ({error_type}), retrying...")
+                        print(f"[ ] Connection lost to Redis ({type(e).__name__}), retrying...")
                         was_disconnected_redis = True
-                # Check for Elasticsearch errors
-                elif "Connection" in error_type or "urllib3" in error_msg or "elastic" in error_msg.lower():
+                elif isinstance(e, (ElasticsearchException, ConnectionError)):
                     if not was_disconnected_es:
-                        print(f"[ ] Connection lost to Elasticsearch ({error_type}), retrying...")
+                        print(f"[ ] Connection lost to Elasticsearch ({type(e).__name__}), retrying...")
                         was_disconnected_es = True

273-274: Multiple bare except clauses may hide unexpected errors.

Bare except: catches all exceptions including SystemExit and KeyboardInterrupt. Consider using except Exception: at minimum for data processing loops.

This applies to lines 273, 308, 317, 328, and similar patterns throughout the file.


146-159: Connection pooling would be more robust.

The current approach recreates Redis clients on failure. Consider using redis.ConnectionPool for automatic connection management and reconnection.

Makefile (1)

35-36: Extensive flake8 ignore list may mask real issues.

The ignore list includes F841 (unused variables), F401 (unused imports), and E722 (bare except). While these may be intentional for now, consider reviewing periodically to catch potential cleanup opportunities.

Consider adding a # TODO comment explaining why these are ignored, or creating a .flake8 config file with inline comments.

.github/workflows/ci.yml (1)

23-27: Consider pinning pip and tool versions for reproducibility.

The workflow upgrades pip and installs latest flake8/pytest versions. This could cause inconsistent behavior over time. Consider pinning versions or using a requirements-dev.txt.

🔧 Suggested improvement
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install flake8 pytest
+          pip install flake8==7.0.0 pytest==8.0.0
           make install

Or maintain a requirements-dev.txt with pinned versions.

docker-compose.yml (3)

1-1: Consider removing deprecated version key.

The version key is obsolete in Docker Compose V2 and later. Modern Docker Compose ignores this field and infers the schema automatically.

Suggested fix
-version: '3.8'
-
 services:

9-10: depends_on doesn't guarantee service readiness.

The depends_on directive only ensures containers start in order, not that Redis or Elasticsearch are accepting connections. Consider adding health checks with condition: service_healthy to avoid race conditions on startup.

Example with health checks
   map_redis:
     image: redis:7-alpine
+    healthcheck:
+      test: ["CMD", "redis-cli", "ping"]
+      interval: 5s
+      timeout: 3s
+      retries: 5
     networks:
       - attack-map-net
 
   elasticsearch:
     image: docker.elastic.co/elasticsearch/elasticsearch:8.18.1
     environment:
       - discovery.type=single-node
       - xpack.security.enabled=false
+    healthcheck:
+      test: ["CMD-SHELL", "curl -s http://localhost:9200/_cluster/health || exit 1"]
+      interval: 10s
+      timeout: 5s
+      retries: 10
     networks:
       - attack-map-net

Then update depends_on:

  depends_on:
    map_redis:
      condition: service_healthy

Also applies to: 21-23


32-35: Redis data is ephemeral without a volume.

Redis data will be lost on container restart. If attack data caching should survive restarts, consider adding a volume mount.

Optional volume for Redis persistence
   map_redis:
     image: redis:7-alpine
+    volumes:
+      - redis_data:/data
     networks:
       - attack-map-net
+
+volumes:
+  redis_data:
static/intelligence.js (2)

89-95: Consider defensive check for msg.protocol.

If msg.protocol is undefined or null, calling .includes(msg.protocol) won't throw but may produce unexpected matches. The generic classification fallback handles this gracefully, but an explicit early return or normalization could improve clarity.


105-113: getSeverityColor could be a static method.

This method doesn't use any instance state (this), so it could be declared as static getSeverityColor(severity) for clearer semantics and to allow calling without an instance.

static/audio-hud.js (1)

95-113: Unused type parameter in trigger().

The type parameter (defaulting to 'traffic') is never used in the method body. The sound played is determined solely by this.selectedSound. Either remove the parameter or implement type-based logic.

If `type` should influence sound selection
     trigger(type = 'traffic') {
         if (!this.enabled) return;
         this.init(); // Late init for browser policy
 
-        switch (this.selectedSound) {
+        // Override sound for critical events
+        const soundToPlay = (type === 'critical') ? 'alert' : this.selectedSound;
+
+        switch (soundToPlay) {
             case 'beep':
static/map.js (1)

160-252: Large conditional block for playback mode.

The isPlayback branch cleanly separates data aggregation logic from visual rendering. This prevents playback from corrupting live state tracking, which is the correct approach.

Consider extracting the data aggregation logic (lines 162-251) into a separate helper function to improve readability and testability.

static/globe-view.js (4)

106-111: Storing HTML in labels creates fragile parsing dependency.

The label property embeds HTML that is later parsed in handlePointHover() using string splits. This couples data storage to display format and will break if the label format changes.

Consider storing structured data separately and generating display content only when needed.

Suggested approach
         const point = {
             lat: attack.src_lat,
             lng: attack.src_long,
             color: attack.color || '#ff0000',
-            label: `<b>Attacker:</b> ${attack.src_ip}<br><b>Country:</b> ${attack.country}`
+            label: attack.src_ip, // Simple label for Globe.gl tooltip
+            // Store structured data for our overlay
+            attackData: {
+                ip: attack.src_ip,
+                country: attack.country
+            }
         };

Then in handlePointHover:

handlePointHover(point) {
    if (point) {
        this.showInsightOverlay('Attacker Profile', {
            'IP': point.attackData.ip,
            'Region': point.attackData.country || 'Unknown'
        });
    }
}

138-148: Fragile string parsing in handleArcHover.

Parsing arc.name with multiple .split() calls is error-prone. If the name format changes or contains unexpected characters (e.g., IP with -> in hostname), parsing will fail silently or produce wrong data.


27-29: External CDN dependency for globe textures.

Globe textures are loaded from unpkg.com. If this CDN is unavailable, the globe will fail to render properly. Consider bundling these assets locally for production reliability, or implementing a fallback.


51-58: Resize event listener is never removed.

The resize event listener is added during init() but there's no cleanup mechanism. If GlobeView instances are created/destroyed dynamically (unlikely but possible), this would cause memory leaks.

For a singleton pattern as used here, this is acceptable, but worth noting if the design changes.

static/index.html (1)

62-63: SRI on same-origin static assets: ensure CI regenerates hashes (or you’ll brick the UI on deploy)

Because these scripts/styles are local and likely to change, hard-coded integrity="sha384-..." (and static/index.css?v=6) needs an automated update mechanism in CI/CD; otherwise browsers will refuse to load updated assets.

Also applies to: 600-610

static/index.css (1)

114-116: Duplicate .hidden utility class — keep a single source of truth

You now define .hidden twice (Line 114-116 and Line 1760-1762). Recommend removing one to avoid future drift.

Also applies to: 1760-1762

static/dashboard.js (1)

3171-3190: Move classification badge styling to CSS classes instead of per-row inline styles

The badge styling (lines 3181–3186) applies inline styles to every row, which is inefficient and harder to maintain. Since getSeverityColor() already has a default case and severity values are controlled by the AttackClassifier.classify() method (limited to low|medium|high|critical), move the styling to CSS classes like severity-low, severity-high, etc., keyed by the severity value in the className (line 3178).

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 276052d and c72ede9.

⛔ Files ignored due to path filters (3)
  • docs/img.png is excluded by !**/*.png
  • static/globe.min.js is excluded by !**/*.min.js
  • static/three.min.js is excluded by !**/*.min.js
📒 Files selected for processing (13)
  • .github/workflows/ci.yml
  • AttackMapServer.py
  • DataServer.py
  • Dockerfile
  • Makefile
  • docker-compose.yml
  • static/audio-hud.js
  • static/dashboard.js
  • static/globe-view.js
  • static/index.css
  • static/index.html
  • static/intelligence.js
  • static/map.js
🧰 Additional context used
🧬 Code graph analysis (1)
static/globe-view.js (1)
static/map.js (6)
  • data (532-532)
  • data (681-681)
  • content (872-872)
  • content (1048-1048)
  • header (848-848)
  • header (1023-1023)
🔇 Additional comments (12)
AttackMapServer.py (1)

23-57: LGTM!

The Redis subscriber implementation with reconnection logic is well-structured. The was_disconnected flag for message deduplication and the retry mechanism with asyncio.sleep(5) are good practices for resilient connections.

Dockerfile (1)

1-6: LGTM on base image and environment setup.

Using python:3.10-slim and setting PYTHONDONTWRITEBYTECODE and PYTHONUNBUFFERED are best practices for containerized Python applications.

DataServer.py (1)

388-425: LGTM!

The check_connections function provides a solid startup health check pattern. The progressive logging (printing "waiting" only once) avoids log spam while keeping users informed.

Makefile (1)

4-4: LGTM!

Using $(PYTHON) -m pip ensures pip runs under the same Python interpreter as the rest of the project, avoiding version mismatches.

.github/workflows/ci.yml (3)

19-21: Python version mismatch between CI and Dockerfile.

CI uses Python 3.11 but the Dockerfile uses Python 3.10. This could lead to code passing CI but failing in production if Python 3.11-specific features are used inadvertently.

Consider aligning the versions:

       - name: Set up Python
         uses: actions/setup-python@v5
         with:
-          python-version: '3.11'
+          python-version: '3.10'
           cache: 'pip'

53-60: Docker image is built but not pushed to a registry.

The build-docker job builds and loads the image locally but doesn't push to any registry. If the intent is to only validate the build, this is fine. Otherwise, you'll need to add registry authentication and a push step.

If publishing is intended, add:

      - name: Login to Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          
      - name: Build and Push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest

38-39: LGTM!

Validating Docker Compose configuration in CI is a good practice to catch syntax errors early.

static/map.js (3)

1532-1533: Placeholder comment suggests incomplete code.

The comment /* ... existing health check logic ... */ appears to be a placeholder. Was health check logic intended here but omitted, or should this comment be removed?


1194-1199: Feature integration pattern looks good.

The optional chaining pattern (if (window.X) window.X.method()) safely integrates the classifier, globe view, and audio HUD without hard dependencies. This allows graceful degradation if any module fails to load.


1535-1563: 2D/3D view toggle implementation is clean.

The toggle correctly manages visibility classes and invokes globeView.show()/hide() appropriately. The button state (text, icon, active class) is updated consistently.

static/index.html (1)

373-387: Playback + Classification markup: IDs/columns align with JS, but prev/next need wiring

The playback bar and Live Feed “Classification” column match the IDs/extra cell created in static/dashboard.js. Only gap: the UI exposes prev/next buttons (playback-prev/playback-next) but current JS doesn’t attach handlers yet.

Also applies to: 513-535

static/index.css (1)

450-457: 3D globe container styling looks consistent with map container

#globe-3d matches the map container sizing and keeps a neutral background for the renderer.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread docker-compose.yml
Comment on lines +37 to +44
# Assuming Elasticsearch is provided externally or defined here
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.18.1
environment:
- discovery.type=single-node
- xpack.security.enabled=false
networks:
- attack-map-net

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

latest Elasticsearch 8.x version 2025

💡 Result:

As of January 15, 2026 the latest Elasticsearch 8.x release is 8.19.9. [1][2]

Sources:

  • Elastic release notes — Elasticsearch 8.19.9. [1]
  • Elastic blog / Elastic Stack 8.19.x release announcements. [2]

Update Elasticsearch version and address security/persistence concerns.

  1. Version 8.18.1 is outdated; update to 8.19.9 (latest 8.x release as of January 2026).
  2. xpack.security.enabled=false disables authentication. Even for local dev, this exposes an unauthenticated Elasticsearch instance if network-accessible.
  3. No volumes defined, so indexed data is lost when the container is removed.

Consider updating the version, and for persistence add a volume mount. Document these limitations if they are intentional for non-production use.

🤖 Prompt for AI Agents
In `@docker-compose.yml` around lines 37 - 44, Update the elasticsearch service:
change the image tag from docker.elastic.co/elasticsearch/elasticsearch:8.18.1
to 8.19.9 (service name: elasticsearch), stop disabling built-in security by
removing or setting xpack.security.enabled to true and supply a development
password (e.g., ELASTIC_PASSWORD) or document that the instance is intentionally
unsecured and restricted to localhost, and add persistent storage by declaring
and mounting a volume for /usr/share/elasticsearch/data (plus add a top-level
named volume entry) so indexed data survives container recreation.

Comment thread static/audio-hud.js
Comment on lines +57 to +74
playCriticalAlert() {
const duration = 0.5;
const osc = this.context.createOscillator();
const gain = this.context.createGain();

osc.frequency.setValueAtTime(440, this.context.currentTime);
osc.frequency.linearRampToValueAtTime(880, this.context.currentTime + duration / 2);
osc.frequency.linearRampToValueAtTime(440, this.context.currentTime + duration);

gain.gain.setValueAtTime(this.masterVolume, this.context.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.context.currentTime + duration);

osc.connect(gain);
gain.connect(this.context.destination);

osc.start();
osc.stop(this.context.currentTime + duration);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing initialization guard can cause runtime error.

playCriticalAlert() accesses this.context without checking this.enabled or this.initialized. If called before initialization (e.g., directly or via trigger() with edge-case timing), this.context will be null, causing a TypeError.

Suggested fix
     playCriticalAlert() {
+        if (!this.enabled || !this.initialized) return;
+        if (this.context.state === 'suspended') this.context.resume();
+
         const duration = 0.5;
         const osc = this.context.createOscillator();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
playCriticalAlert() {
const duration = 0.5;
const osc = this.context.createOscillator();
const gain = this.context.createGain();
osc.frequency.setValueAtTime(440, this.context.currentTime);
osc.frequency.linearRampToValueAtTime(880, this.context.currentTime + duration / 2);
osc.frequency.linearRampToValueAtTime(440, this.context.currentTime + duration);
gain.gain.setValueAtTime(this.masterVolume, this.context.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.context.currentTime + duration);
osc.connect(gain);
gain.connect(this.context.destination);
osc.start();
osc.stop(this.context.currentTime + duration);
}
playCriticalAlert() {
if (!this.enabled || !this.initialized) return;
if (this.context.state === 'suspended') this.context.resume();
const duration = 0.5;
const osc = this.context.createOscillator();
const gain = this.context.createGain();
osc.frequency.setValueAtTime(440, this.context.currentTime);
osc.frequency.linearRampToValueAtTime(880, this.context.currentTime + duration / 2);
osc.frequency.linearRampToValueAtTime(440, this.context.currentTime + duration);
gain.gain.setValueAtTime(this.masterVolume, this.context.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01, this.context.currentTime + duration);
osc.connect(gain);
gain.connect(this.context.destination);
osc.start();
osc.stop(this.context.currentTime + duration);
}
🤖 Prompt for AI Agents
In `@static/audio-hud.js` around lines 57 - 74, playCriticalAlert() currently uses
this.context directly and can throw if audio isn't ready; add an initialization
guard at the start of playCriticalAlert (check this.enabled and this.initialized
and that this.context is non-null) and return early if not initialized (or call
the existing init/initialize method if appropriate), ensuring the method never
dereferences this.context when audio is not ready; also update any callers like
trigger() to respect the same guard if they invoke playCriticalAlert.

Comment thread static/audio-hud.js
Comment thread static/dashboard.js
Comment on lines +3779 to +3806
/**
* Phase 3: Visual Alerts & Pulse Effects
* Triggers UI animations based on threat severity
*/
triggerVisualAlert(severity) {
if (!['critical', 'high'].includes(severity)) return;

const navbar = document.querySelector('.top-navbar');
const pulseClass = severity === 'critical' ? 'pulse-critical' : 'pulse-high';

// Pulse the navbar
if (navbar) {
navbar.classList.add(pulseClass);
setTimeout(() => navbar.classList.remove(pulseClass), 5000);
}

// Pulse the Dashboard tab button if not active
const dashTab = document.querySelector('[data-tab="overview"]');
if (dashTab && !dashTab.classList.contains('active')) {
dashTab.classList.add('pulse-high');
setTimeout(() => dashTab.classList.remove('pulse-high'), 3000);
}

// Notification for critical events
if (severity === 'critical') {
console.log('%c [CRITICAL ALERT] High-severity attack detected! ', 'background: #dc3545; color: #fff; font-weight: bold;');
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Visual alert: use the severity-matched pulse class consistently

For critical, you compute pulseClass (Line 3787) but the overview tab always gets pulse-high (Line 3798). If you want consistent semantics, use pulseClass there too (and normalize severity at the call site, which is addressed above).

Proposed fix
 const dashTab = document.querySelector('[data-tab="overview"]');
 if (dashTab && !dashTab.classList.contains('active')) {
-    dashTab.classList.add('pulse-high');
-    setTimeout(() => dashTab.classList.remove('pulse-high'), 3000);
+    dashTab.classList.add(pulseClass);
+    setTimeout(() => dashTab.classList.remove(pulseClass), 3000);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Phase 3: Visual Alerts & Pulse Effects
* Triggers UI animations based on threat severity
*/
triggerVisualAlert(severity) {
if (!['critical', 'high'].includes(severity)) return;
const navbar = document.querySelector('.top-navbar');
const pulseClass = severity === 'critical' ? 'pulse-critical' : 'pulse-high';
// Pulse the navbar
if (navbar) {
navbar.classList.add(pulseClass);
setTimeout(() => navbar.classList.remove(pulseClass), 5000);
}
// Pulse the Dashboard tab button if not active
const dashTab = document.querySelector('[data-tab="overview"]');
if (dashTab && !dashTab.classList.contains('active')) {
dashTab.classList.add('pulse-high');
setTimeout(() => dashTab.classList.remove('pulse-high'), 3000);
}
// Notification for critical events
if (severity === 'critical') {
console.log('%c [CRITICAL ALERT] High-severity attack detected! ', 'background: #dc3545; color: #fff; font-weight: bold;');
}
}
/**
* Phase 3: Visual Alerts & Pulse Effects
* Triggers UI animations based on threat severity
*/
triggerVisualAlert(severity) {
if (!['critical', 'high'].includes(severity)) return;
const navbar = document.querySelector('.top-navbar');
const pulseClass = severity === 'critical' ? 'pulse-critical' : 'pulse-high';
// Pulse the navbar
if (navbar) {
navbar.classList.add(pulseClass);
setTimeout(() => navbar.classList.remove(pulseClass), 5000);
}
// Pulse the Dashboard tab button if not active
const dashTab = document.querySelector('[data-tab="overview"]');
if (dashTab && !dashTab.classList.contains('active')) {
dashTab.classList.add(pulseClass);
setTimeout(() => dashTab.classList.remove(pulseClass), 3000);
}
// Notification for critical events
if (severity === 'critical') {
console.log('%c [CRITICAL ALERT] High-severity attack detected! ', 'background: `#dc3545`; color: `#fff`; font-weight: bold;');
}
}
🤖 Prompt for AI Agents
In `@static/dashboard.js` around lines 3779 - 3806, In triggerVisualAlert, the
overview tab is hardcoded to use 'pulse-high' instead of the computed
pulseClass; change the dashTab handling to add and later remove pulseClass (not
the literal 'pulse-high') so the tab pulse matches the navbar pulseClass,
referencing the existing pulseClass and dashTab symbols and keeping the existing
timeouts.

Comment thread static/globe-view.js
Comment thread static/index.css
Comment on lines +2970 to +3133
/* Playback Bar */
.playback-bar {
position: fixed;
bottom: 350px;
left: 0;
right: 200px;
height: 60px;
background: rgba(10, 10, 10, 0.95);
backdrop-filter: blur(10px);
border-top: 2px solid var(--primary-color);
padding: 0 var(--spacing-lg);
z-index: 1000;
display: flex;
align-items: center;
justify-content: space-between;
animation: slideUp 0.3s ease;
box-shadow: 0 -4px 30px rgba(0,0,0,0.6);
}

.playback-bar.hidden {
display: none !important;
}

.playback-info {
display: flex;
align-items: center;
gap: var(--spacing-md);
}

.playback-badge {
background: var(--primary-color);
color: white;
padding: 3px 10px;
font-size: 11px;
font-weight: 800;
border-radius: 4px;
letter-spacing: 1.5px;
box-shadow: 0 0 10px var(--glow);
}

#playback-time {
font-family: var(--font-mono);
font-size: var(--font-sm);
color: var(--text-primary);
}

.playback-scrubber {
flex: 1;
display: flex;
align-items: center;
max-width: 600px;
gap: var(--spacing-lg);
}

#playback-slider {
flex: 1;
accent-color: var(--primary-color);
height: 6px;
cursor: pointer;
background: var(--bg-tertiary);
border-radius: 3px;
appearance: none;
}

#playback-slider::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
background: var(--text-primary);
border: 2px solid var(--primary-color);
border-radius: 50%;
box-shadow: 0 0 10px var(--glow);
}

.playback-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: var(--font-lg);
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}

.playback-btn:hover {
color: var(--primary-color);
transform: scale(1.1);
}

.playback-actions {
display: flex;
gap: var(--spacing-md);
}

@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}

/* Phase 3: Visual Pulse Effects */
.pulse-critical {
animation: criticalPulse 2s infinite;
}

.pulse-high {
animation: highPulse 2s infinite;
}

@keyframes criticalPulse {
0% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0.4); }
70% { box-shadow: 0 0 0 15px rgba(220, 53, 69, 0); }
100% { box-shadow: 0 0 0 0 rgba(220, 53, 69, 0); }
}

@keyframes highPulse {
0% { box-shadow: 0 0 0 0 rgba(255, 102, 0, 0.3); }
70% { box-shadow: 0 0 0 10px rgba(255, 102, 0, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 102, 0, 0); }
}

/* Dashboard Card HUD Updates */
.dashboard-card.alert-active {
border-color: var(--primary-color) !important;
background: rgba(226, 0, 116, 0.05);
}

/* Contextual Insights Tooltip (Globe) */
.globe-insight-overlay {
position: absolute;
top: 80px;
right: 220px;
width: 280px;
background: rgba(10, 10, 10, 0.85);
backdrop-filter: blur(15px);
border: 1px solid var(--border-primary);
border-left: 3px solid var(--primary-color);
border-radius: var(--radius-md);
padding: var(--spacing-md);
color: white;
z-index: 500;
pointer-events: none;
transition: opacity 0.3s ease, transform 0.3s ease;
}

.insight-header {
font-size: var(--font-xs);
font-weight: 800;
color: var(--primary-color);
text-transform: uppercase;
letter-spacing: 1.5px;
margin-bottom: var(--spacing-sm);
}

.insight-item {
display: flex;
justify-content: space-between;
font-size: var(--font-sm);
margin-bottom: 4px;
}

.insight-label { color: var(--text-secondary); }
.insight-value { font-weight: 600; font-family: var(--font-mono); }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Playback bar / globe overlay positioning is hard-coded and will drift with resizing + responsive layout

bottom: 350px and right: 200px assume fixed bottom-panel height and side-panel width; but the bottom panel is user-resizable and the side-panel becomes off-canvas in responsive modes—so the playback bar / insight overlay will be misplaced on many layouts. Suggest driving these via CSS variables that JS updates when panels resize/collapse (e.g., --bottom-panel-height, --side-panel-width).

Proposed direction (CSS side)
 :root {
+    /* Keep in sync with dashboard panel sizing */
+    --bottom-panel-height: 350px;
+    --side-panel-width: 200px;
 }
 .playback-bar {
-    bottom: 350px;
+    bottom: var(--bottom-panel-height);
     left: 0;
-    right: 200px;
+    right: var(--side-panel-width);
@@
 }
@@
 .globe-insight-overlay {
@@
-    right: 220px;
+    right: calc(var(--side-panel-width) + 20px);
 }

(Requires JS to update these variables when the user resizes/collapses panels.)

🤖 Prompt for AI Agents
In `@static/index.css` around lines 2970 - 3133, The playback bar (.playback-bar)
and globe overlay (.globe-insight-overlay) use hard-coded offsets (bottom:
350px; right: 200px) which break with resizable/collapsible panels—replace those
fixed values with CSS variables (e.g., bottom: calc(var(--bottom-panel-height,
350px) + 0px); right: calc(var(--side-panel-width, 200px) + 0px)) so positioning
is driven by --bottom-panel-height and --side-panel-width, and update any
related z-index/transitions to remain unchanged; also add default fallback
values in the var() calls and ensure the UI JS (panel resize/collapse handlers)
sets document.documentElement.style.setProperty('--bottom-panel-height', 'Xpx')
and '--side-panel-width' accordingly whenever panels resize or toggle (affecting
both .playback-bar and .globe-insight-overlay).

Comment thread static/index.html
Comment on lines +7 to +8
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self'; style-src 'self'; font-src 'self'; img-src 'self' https://*.cartocdn.com https://*.openstreetmap.org data:; object-src 'none'; connect-src 'self' wss: ws:">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the full content of static/index.html to understand the CSP and the application
cat -n static/index.html | head -20

Repository: khulnasoft/cyberpot-attack-map

Length of output: 1271


🏁 Script executed:

# Look for websocket usage and Globe.gl/Three.js configuration
grep -r "wss\|ws\|Globe\|three" static/ --include="*.html" --include="*.js" -n | head -50

Repository: khulnasoft/cyberpot-attack-map

Length of output: 50388


🌐 Web query:

Globe.gl Three.js CSP Content Security Policy requirements worker-src unsafe-eval

💡 Result:

Short answer: globe.gl (which uses Three.js/three-globe) may create Blob-based workers and use dynamic eval/WebAssembly features; to avoid CSP failures you must allow blob: for workers and permit eval/wasm when needed. Concretely add worker-src blob: (or include blob: in default/script fallbacks) and include 'unsafe-eval' and/or 'wasm-unsafe-eval' in script-src if your build/runtime uses eval-like APIs or WebAssembly compilation. (globe.gl)

Suggested policy examples:

  • If you need blob workers and eval:
    Content-Security-Policy: worker-src blob:; script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval' https:; object-src 'none'; frame-ancestors 'none'. (developer.mozilla.org)

  • More restrictive (only allow blob for workers, avoid global unsafe-eval if possible):
    Content-Security-Policy: worker-src blob:; script-src 'self' 'nonce-...'; trusted-types-eval 'strict-dynamic' 'nonce-...'; ...
    (Use nonces/hashes or trusted types where you can to reduce reliance on 'unsafe-eval'.) (developer.mozilla.org)

Notes and links:

  • Blob URLs must be allowed explicitly (worker-src blob:) because blob: does not match * or other hosts. (stackoverflow.com)
  • 'unsafe-eval' is insecure; prefer wasm-unsafe-eval for WebAssembly-only needs or trusted-types alternatives when supported. (developer.mozilla.org)

If you want, tell me how you load globe.gl (CDN vs bundler, running in extension/Electron/iframe) and I’ll produce the minimal CSP header/meta tag tailored to your setup with exact directives.

Citations:


🌐 Web query:

CSP connect-src ws: wss: websocket security implications

💡 Result:

Short answer

  • Allowing ws: or wss: in connect-src permits WebSocket connections from any host using that scheme; ws: is unencrypted (eavesdropping / MITM risk), wss: is encrypted but still allows arbitrary endpoints unless you constrain hosts. [1][2][3]

Security implications and guidance

  • ws: (insecure) — traffic is cleartext; use only for local/dev or within trusted networks. Never use ws: in production. [2]
  • wss: (secure) — encrypts transport (TLS) but does not prevent connecting to malicious servers or servers that accept connections from untrusted origins. You must still validate/authenticate on the server and restrict accepted origins. [2][4]
  • connect-src controls which endpoints the browser may open (fetch/XHR/fetch(), EventSource and WebSocket). It does not bypass or replace CORS/origin checks on the server. A permissive ws:/wss: value effectively allows pages to open sockets to any host that matches the pattern. [1][3]
  • Risks if you allow broad ws:/wss::
    • Data exfiltration to attacker-controlled endpoints.
    • Unencrypted eavesdropping and tampering (ws:).
    • Server-side logic exploited via malformed messages or injection if messages aren’t validated.
    • DoS by opening many connections or large frames. [2][4]

Practical mitigations

  • Prefer explicit host allow-listing instead of scheme-only:
    Example: Content-Security-Policy: connect-src 'self' wss://api.example.com wss://realtime.cdn.example.net;
  • Enforce wss:// in production (block ws:). [2]
  • On the WebSocket server: validate Origin header, require strong authentication/authorization (tokens, short TTL), rate-limit/throttle connections, disable unsafe compression (permessage-deflate) unless necessary, and validate message payloads. [2][4]
  • Monitor/report CSP violations and use least privilege (only permit required endpoints). [3]

If you want, I can review your current CSP header and suggest a minimally permissive connect-src value.


🏁 Script executed:

# Check if globe.gl or three.js are referenced in the HTML
grep -i "globe\|three" static/index.html -n | head -20

Repository: khulnasoft/cyberpot-attack-map

Length of output: 525


🏁 Script executed:

# Check for any WebSocket initialization or actual connect-src usage
grep -rn "WebSocket\|wss\|ws://" static/ --include="*.js" --include="*.html" 2>/dev/null | head -30

Repository: khulnasoft/cyberpot-attack-map

Length of output: 2871


🏁 Script executed:

# Look for the mentioned additions from the scratchpad (3D globe, playback bar, etc.)
grep -n "globe-3d\|playback\|Classification" static/index.html

Repository: khulnasoft/cyberpot-attack-map

Length of output: 974


CSP: Missing worker-src blob: for Globe.gl; tighten connect-src to specific endpoint

Globe.gl (loaded at line 56-59) may create Blob-based workers that will fail without worker-src blob:. While connect-src 'self' wss: ws: technically permits WebSocket connections to any host using those schemes, the application only connects to a single WS_HOST endpoint (map.js:1300). Consider:

  1. Add worker-src blob: to support Globe.gl's worker pattern
  2. Restrict connect-src to the specific WebSocket endpoint (e.g., wss://your-api.example.com) if known at build time, or validate/whitelist server-side
  3. Add frame-ancestors 'none' and base-uri 'self' to reduce clickjacking/injection risk
🤖 Prompt for AI Agents
In `@static/index.html` around lines 7 - 8, Update the page
Content-Security-Policy meta by adding worker-src 'blob:' to allow Globe.gl's
Blob-based workers, tighten connect-src from the broad "wss: ws:" to the
specific WebSocket origin used by your app (the endpoint referenced in map.js at
the connection logic around the WS_HOST usage) or a build-time/whitelisted host,
and add frame-ancestors 'none' and base-uri 'self' to the CSP string; modify the
content value in the meta tag (the attribute containing default-src...
connect-src...) to include these directives while preserving existing allowed
sources (script-src, style-src, img-src, etc.).

Comment thread static/intelligence.js
Comment on lines +58 to +64
{
id: 'brute_force',
name: 'Brute Force Attempt',
description: 'Repeated connection attempts to authentication services.',
criteria: (msg) => ['SSH', 'TELNET', 'FTP', 'RDP', 'ADB', 'VNC', 'RLOGIN'].includes(msg.protocol),
severity: 'medium'
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Duplicate brute_force pattern is unreachable.

This pattern (lines 58-64) duplicates the one at lines 9-15 with an expanded protocol list (VNC, RLOGIN added). Due to the first-match logic in classify(), this second pattern will never be evaluated for protocols already covered by the first pattern.

Either remove this duplicate or merge the protocol lists into the first pattern.

Suggested fix: merge protocol lists
             {
                 id: 'brute_force',
                 name: 'Brute Force Attempt',
                 description: 'Repeated connection attempts to authentication services.',
-                criteria: (msg) => ['SSH', 'TELNET', 'FTP', 'RDP', 'ADB'].includes(msg.protocol),
+                criteria: (msg) => ['SSH', 'TELNET', 'FTP', 'RDP', 'ADB', 'VNC', 'RLOGIN'].includes(msg.protocol),
                 severity: 'medium'
             },

Then remove lines 58-64 entirely.

🤖 Prompt for AI Agents
In `@static/intelligence.js` around lines 58 - 64, There is a duplicate pattern
object with id 'brute_force' that is unreachable because classify() uses
first-match; merge the expanded protocol list (add 'VNC' and 'RLOGIN') into the
existing 'brute_force' pattern definition (the earlier object with id
'brute_force' and criteria function) and then delete the later duplicate pattern
object so only one 'brute_force' entry remains.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.github/workflows/docker-publish.yml:
- Around line 42-46: Update the cosign installation step to use current
releases: change the action reference from
sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 to a modern
tag (for example sigstore/cosign-installer@v4.0.0 or `@v3.10.1`) and update the
cosign-release input from 'v2.2.4' to a current cosign release (for example
'v3.0.4' or at minimum 'v2.6.2'); ensure the job using the cosign-installer
action and the cosign-release input are both updated consistently so the
installer installs a supported cosign binary.
🧹 Nitpick comments (6)
AttackMapServer.py (1)

17-18: Missing commented production configuration variants.

Per coding guidelines, both production and local configuration variants should be preserved in connection strings, with the active config uncommented and production config commented out. The current implementation uses only environment variables without the commented alternatives.

Based on coding guidelines, consider preserving the pattern:

Suggested change
 # Configuration
+# Production config (uncomment for production):
+# redis_url = 'redis://redis:6379'
+# Local development config:
 redis_url = os.getenv('MAP_REDIS_URL', 'redis://127.0.0.1:6379')
 web_port = int(os.getenv('MAP_WEB_PORT', '64299'))
DataServer.py (3)

10-15: Missing commented production configuration variants per coding guidelines.

The coding guidelines require preserving both production and local configuration variants, with the active config uncommented and production config commented out. Currently, only environment-variable-based configuration is present without the documented production defaults.

Based on learnings, consider adding commented production variants:

Suggested documentation pattern
 # Configuration
+# Production configuration (uncomment for production deployment):
+# es_url = 'http://elasticsearch:9200'
+# redis_ip = 'redis'
+# Local/development configuration:
 es_url = os.getenv('MAP_ES_URL', 'http://127.0.0.1:9200')
-es = Elasticsearch(es_url)
 redis_ip = os.getenv('MAP_REDIS_HOST', '127.0.0.1')
+es = Elasticsearch(es_url)

229-236: Consider using explicit datetime formatting.

The current approach of converting to naive datetime and splitting strings works, but is fragile. Using strftime() or isoformat() would be more explicit.

Optional: Cleaner formatting
         # Get the last 100 new honeypot events every 0.5s
-        # Convert timezone-aware datetime to naive for consistent string formatting with ES
-        mylast_dt = time_last_request.replace(tzinfo=None)
-        mynow_dt = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=mydelta)).replace(tzinfo=None)
-        
-        mylast = str(mylast_dt).split(" ")
-        mynow = str(mynow_dt).split(" ")
+        mylast_dt = time_last_request.replace(tzinfo=None)
+        mynow_dt = (datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(seconds=mydelta)).replace(tzinfo=None)
+        
+        mylast_str = mylast_dt.strftime("%Y-%m-%dT%H:%M:%S.%f")
+        mynow_str = mynow_dt.strftime("%Y-%m-%dT%H:%M:%S.%f")

Then use mylast_str and mynow_str directly in the range query instead of mylast[0] + "T" + mylast[1].


339-342: Minor inconsistency: mixed UTC constants.

Line 341 uses pytz.UTC while the rest of the file now uses datetime.timezone.utc. Consider unifying for consistency:

-            my_time = my_time.replace(tzinfo=pytz.UTC)  # Assuming event_time is in UTC
+            my_time = my_time.replace(tzinfo=datetime.timezone.utc)  # Assuming event_time is in UTC

Note: The pytz import would still be needed if get_localzone() from tzlocal returns pytz-based timezones.

.github/workflows/docker-publish.yml (2)

37-38: Consider SHA-pinning checkout action for consistency.

Other actions in this workflow are SHA-pinned for supply-chain security, but actions/checkout uses a version tag (v4). For consistency, consider pinning to a specific SHA.


51-52: Update Docker action versions to current stable releases.

The workflow uses SHA-pinned Docker actions, but the versions are significantly outdated:

  • setup-buildx-action: v3.0.0 → v3.12.0
  • login-action: v3.0.0 → v3.6.0
  • metadata-action: v5.0.0 → v5.10.0
  • build-push-action: v5.0.0 → v6.18.0 (major version behind)

Updating to the latest versions will include security patches and new features, particularly for build-push-action which has a major version update available.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c72ede9 and c899841.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • .github/workflows/docker-publish.yml
  • AttackMapServer.py
  • DataServer.py
  • tests/test_DataServer.py
💤 Files with no reviewable changes (1)
  • tests/test_DataServer.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci.yml
🧰 Additional context used
📓 Path-based instructions (3)
**/{DataServer,AttackMapServer}.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/{DataServer,AttackMapServer}.py: Preserve both production and local configuration variants in connection strings (Redis URL). Active config is uncommented, production config is commented out.
Redis Pub/Sub must use single channel attack-map-production for publishing attack events and statistics from DataServer to all connected WebSocket clients

Files:

  • DataServer.py
  • AttackMapServer.py
**/DataServer.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/DataServer.py: Protocol-to-color mapping (service_rgb dictionary) must be maintained in DataServer.py only. When adding a protocol, update both port_to_type() function and service_rgb dictionary in DataServer.py.
DataServer.py must be 100% synchronous: no async/await, use time.sleep() for delays, and redis.StrictRedis client
DataServer.py must poll Elasticsearch every 0.5 seconds for the last 100 events and every 10 seconds for statistics aggregations (1m/1h/24h)
Elasticsearch queries must use logstash-* indices to retrieve CyberPot honeypot events and enforce maximum 100 events per poll

Files:

  • DataServer.py
**/AttackMapServer.py

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/AttackMapServer.py: AttackMapServer.py must be 100% async: use await, asyncio.create_task, and redis.asyncio client
WebSocket server must serve static files from static/ directory and listen on configurable port (default 1234)

Files:

  • AttackMapServer.py
🧠 Learnings (9)
📚 Learning: 2026-01-15T12:25:45.745Z
Learnt from: CR
Repo: khulnasoft/cyberpot-attack-map PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-15T12:25:45.745Z
Learning: Applies to **/{DataServer,AttackMapServer}.py : Preserve both production and local configuration variants in connection strings (Redis URL). Active config is uncommented, production config is commented out.

Applied to files:

  • DataServer.py
  • AttackMapServer.py
📚 Learning: 2026-01-15T12:25:45.745Z
Learnt from: CR
Repo: khulnasoft/cyberpot-attack-map PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-15T12:25:45.745Z
Learning: Applies to **/DataServer.py : DataServer.py must poll Elasticsearch every 0.5 seconds for the last 100 events and every 10 seconds for statistics aggregations (1m/1h/24h)

Applied to files:

  • DataServer.py
📚 Learning: 2026-01-15T12:25:45.745Z
Learnt from: CR
Repo: khulnasoft/cyberpot-attack-map PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-15T12:25:45.745Z
Learning: Applies to **/DataServer.py : DataServer.py must be 100% synchronous: no async/await, use time.sleep() for delays, and redis.StrictRedis client

Applied to files:

  • DataServer.py
  • AttackMapServer.py
📚 Learning: 2026-01-15T12:25:45.745Z
Learnt from: CR
Repo: khulnasoft/cyberpot-attack-map PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-15T12:25:45.745Z
Learning: When debugging data flow issues: check DataServer.py console for Elasticsearch errors if no attacks show, check Redis connection in AttackMapServer.py for WebSocket disconnects, and add port mappings in port_to_type() if protocol shows as OTHER

Applied to files:

  • DataServer.py
  • AttackMapServer.py
📚 Learning: 2026-01-15T12:25:45.745Z
Learnt from: CR
Repo: khulnasoft/cyberpot-attack-map PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-15T12:25:45.745Z
Learning: Local development setup requires Redis (port 6379), Elasticsearch (via SSH tunnel on port 64298), DataServer.py, and AttackMapServer.py running in separate terminal sessions

Applied to files:

  • DataServer.py
  • AttackMapServer.py
📚 Learning: 2026-01-15T12:25:45.745Z
Learnt from: CR
Repo: khulnasoft/cyberpot-attack-map PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-15T12:25:45.745Z
Learning: Applies to **/{DataServer,AttackMapServer}.py : Redis Pub/Sub must use single channel attack-map-production for publishing attack events and statistics from DataServer to all connected WebSocket clients

Applied to files:

  • DataServer.py
  • AttackMapServer.py
📚 Learning: 2026-01-15T12:25:45.745Z
Learnt from: CR
Repo: khulnasoft/cyberpot-attack-map PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-15T12:25:45.745Z
Learning: Applies to **/DataServer.py : Elasticsearch queries must use logstash-* indices to retrieve CyberPot honeypot events and enforce maximum 100 events per poll

Applied to files:

  • DataServer.py
📚 Learning: 2026-01-15T12:25:45.745Z
Learnt from: CR
Repo: khulnasoft/cyberpot-attack-map PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-15T12:25:45.745Z
Learning: Applies to **/AttackMapServer.py : AttackMapServer.py must be 100% async: use await, asyncio.create_task, and redis.asyncio client

Applied to files:

  • DataServer.py
  • AttackMapServer.py
📚 Learning: 2026-01-15T12:25:45.745Z
Learnt from: CR
Repo: khulnasoft/cyberpot-attack-map PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-01-15T12:25:45.745Z
Learning: Applies to **/AttackMapServer.py : WebSocket server must serve static files from static/ directory and listen on configurable port (default 1234)

Applied to files:

  • AttackMapServer.py
🔇 Additional comments (6)
AttackMapServer.py (2)

18-18: Verify the default port value.

The default port is set to 64299, but the learnings indicate the WebSocket server should "listen on configurable port (default 1234)". Please confirm if the default has been intentionally changed to 64299 (aligning with docker-compose.yml) and update the documentation accordingly, or if 1234 should remain the default.


14-19: Environment-based configuration and version bump look good.

The use of os.getenv() for configuration aligns well with the containerization goals (Docker/CI) mentioned in the PR objectives. The version bump to 3.0.0 appropriately reflects the major feature additions.

DataServer.py (2)

210-214: LGTM! Good compatibility improvement.

Using datetime.timezone.utc instead of datetime.UTC improves compatibility with Python versions before 3.11, which is a sensible choice for broader deployment support.


264-278: Polling and limits correctly implemented per guidelines.

The implementation correctly follows the coding guidelines:

  • Polls Elasticsearch every 0.5 seconds (time.sleep(0.5))
  • Retrieves maximum 100 events (size=100)
  • Uses logstash-* index pattern
  • Statistics aggregation runs every 10 seconds (line 216)

Based on learnings, this aligns with the required DataServer.py behavior.

.github/workflows/docker-publish.yml (2)

90-98: Signing configuration looks secure.

The signing step correctly:

  • Uses intermediate environment variables to prevent injection attacks (per GitHub's security hardening guidance)
  • Leverages OIDC/Fulcio for keyless signing
  • Skips execution on pull requests

1-98: Well-structured Docker publish workflow.

The workflow follows good practices:

  • SHA-pinned actions for supply-chain security
  • Appropriate permission scoping (least-privilege)
  • Conditional execution to avoid pushing/signing on PRs
  • GHA cache for build optimization
  • Sigstore integration for image provenance

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +42 to +46
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
with:
cosign-release: 'v2.2.4'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check latest cosign-installer and cosign releases

echo "=== Latest cosign-installer releases ==="
gh api repos/sigstore/cosign-installer/releases --jq '.[0:5] | .[] | "\(.tag_name) - \(.published_at)"'

echo ""
echo "=== Latest cosign releases ==="
gh api repos/sigstore/cosign/releases --jq '.[0:5] | .[] | "\(.tag_name) - \(.published_at)"'

Repository: khulnasoft/cyberpot-attack-map

Length of output: 448


Update cosign-installer and cosign to current versions.

The workflow uses cosign-installer@v3.5.0 and cosign-release: v2.2.4, both of which are significantly outdated. Latest available versions are cosign-installer@v4.0.0 (or v3.10.1 for v3 branch) and cosign@v3.0.4 (or v2.6.2 for v2 branch). Supply-chain security tools should be kept current to benefit from security fixes and improvements. Consider updating to at least v3.10.1+ and v2.6.2+ respectively.

🤖 Prompt for AI Agents
In @.github/workflows/docker-publish.yml around lines 42 - 46, Update the cosign
installation step to use current releases: change the action reference from
sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 to a modern
tag (for example sigstore/cosign-installer@v4.0.0 or `@v3.10.1`) and update the
cosign-release input from 'v2.2.4' to a current cosign release (for example
'v3.0.4' or at minimum 'v2.6.2'); ensure the job using the cosign-installer
action and the cosign-release input are both updated consistently so the
installer installs a supported cosign binary.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com>
@vercel

vercel Bot commented Feb 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v0-cyberpot-attack-map Error Error Aug 24, 2026 11:22pm

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant