feat: implement advanced dashboard enhancements and devops automation - #2
feat: implement advanced dashboard enhancements and devops automation#2khulnasoft-bot wants to merge 4 commits into
Conversation
- 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
|
Warning Review limit reachedNext included review available in 58 minutes. View limit detailsLimit 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. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
WalkthroughAdds 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
Sequence DiagramssequenceDiagram
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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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()setsslider.max = this.playbackHistory.length - 1(Line 3242) which becomes-1when the cache is empty.prevBtn/nextBtnare fetched (Line 3208-3209) but never used.exitBtn.onclick = ...(Line 3213) can throw if the element is missing even thoughslider/playbackBarexist.slider.oninputwill callupdatePlaybackView()on every scrub event (Line 3215-3218); ifprocessRestoredAttackis 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: usesetSound()method instead of direct property assignment, and sync sound dropdown with audioHud
applySettings()setswindow.audioHud.selectedSounddirectly (line 2152-2153) butsetSound()method exists and should be used instead. More critically, the alert-sound change handler (lines 2307-2317) updates settings and localStorage but never callsapplySettings()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 sensitivityThe
addAttackEvent()method lacks a guard forplaybackModeand 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 forMAP_WEB_PORTenvironment variable.If
MAP_WEB_PORTcontains a non-numeric value,int()will raise aValueErrorand 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: Bareexceptclause 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.dockerignorefile.
COPY . .will include all files including.git/,__pycache__/, local config files, and potentially sensitive data. A.dockerignorefile 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 64299DataServer.py (4)
10-13: Module-level Elasticsearch initialization may fail silently at import time.The
Elasticsearchclient is initialized immediately when the module loads. IfMAP_ES_URLis 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 esThen use
get_es_client()whereesis 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 bareexceptclauses may hide unexpected errors.Bare
except:catches all exceptions includingSystemExitandKeyboardInterrupt. Consider usingexcept 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.ConnectionPoolfor 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), andE722(bare except). While these may be intentional for now, consider reviewing periodically to catch potential cleanup opportunities.Consider adding a
# TODOcomment explaining why these are ignored, or creating a.flake8config 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 installOr maintain a
requirements-dev.txtwith pinned versions.docker-compose.yml (3)
1-1: Consider removing deprecatedversionkey.The
versionkey 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_ondoesn't guarantee service readiness.The
depends_ondirective only ensures containers start in order, not that Redis or Elasticsearch are accepting connections. Consider adding health checks withcondition: service_healthyto 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-netThen update
depends_on:depends_on: map_redis: condition: service_healthyAlso 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 formsg.protocol.If
msg.protocolisundefinedornull, 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:getSeverityColorcould be a static method.This method doesn't use any instance state (
this), so it could be declared asstatic getSeverityColor(severity)for clearer semantics and to allow calling without an instance.static/audio-hud.js (1)
95-113: Unusedtypeparameter intrigger().The
typeparameter (defaulting to'traffic') is never used in the method body. The sound played is determined solely bythis.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
isPlaybackbranch 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
labelproperty embeds HTML that is later parsed inhandlePointHover()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 inhandleArcHover.Parsing
arc.namewith 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
resizeevent listener is added duringinit()but there's no cleanup mechanism. IfGlobeViewinstances 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-..."(andstatic/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.hiddenutility class — keep a single source of truthYou now define
.hiddentwice (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 stylesThe 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 theAttackClassifier.classify()method (limited tolow|medium|high|critical), move the styling to CSS classes likeseverity-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
⛔ Files ignored due to path filters (3)
docs/img.pngis excluded by!**/*.pngstatic/globe.min.jsis excluded by!**/*.min.jsstatic/three.min.jsis excluded by!**/*.min.js
📒 Files selected for processing (13)
.github/workflows/ci.ymlAttackMapServer.pyDataServer.pyDockerfileMakefiledocker-compose.ymlstatic/audio-hud.jsstatic/dashboard.jsstatic/globe-view.jsstatic/index.cssstatic/index.htmlstatic/intelligence.jsstatic/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_disconnectedflag for message deduplication and the retry mechanism withasyncio.sleep(5)are good practices for resilient connections.Dockerfile (1)
1-6: LGTM on base image and environment setup.Using
python:3.10-slimand settingPYTHONDONTWRITEBYTECODEandPYTHONUNBUFFEREDare best practices for containerized Python applications.DataServer.py (1)
388-425: LGTM!The
check_connectionsfunction 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 pipensures 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-dockerjob 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 wiringThe 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-3dmatches 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.
| # 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 |
There was a problem hiding this comment.
🧩 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.
- Version 8.18.1 is outdated; update to 8.19.9 (latest 8.x release as of January 2026).
xpack.security.enabled=falsedisables authentication. Even for local dev, this exposes an unauthenticated Elasticsearch instance if network-accessible.- 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| /** | ||
| * 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;'); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| /** | |
| * 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.
| /* 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); } |
There was a problem hiding this comment.
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).
| <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:"> |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -50Repository: 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:
- 1: https://globe.gl/?utm_source=openai
- 2: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src?utm_source=openai
- 3: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy?utm_source=openai
- 4: https://stackoverflow.com/questions/54695310/getting-refused-to-create-worker-from-blob-error-in-video-min-js-when-looking?utm_source=openai
- 5: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src?utm_source=openai
🌐 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 -20Repository: 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 -30Repository: 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.htmlRepository: 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:
- Add
worker-src blob:to support Globe.gl's worker pattern - Restrict
connect-srcto the specific WebSocket endpoint (e.g.,wss://your-api.example.com) if known at build time, or validate/whitelist server-side - Add
frame-ancestors 'none'andbase-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.).
| { | ||
| 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' | ||
| }, |
There was a problem hiding this comment.
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.
Release 3.0.0
There was a problem hiding this comment.
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()orisoformat()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_strandmynow_strdirectly in the range query instead ofmylast[0] + "T" + mylast[1].
339-342: Minor inconsistency: mixed UTC constants.Line 341 uses
pytz.UTCwhile the rest of the file now usesdatetime.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 UTCNote: The
pytzimport would still be needed ifget_localzone()fromtzlocalreturns 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/checkoutuses 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.0login-action: v3.0.0 → v3.6.0metadata-action: v5.0.0 → v5.10.0build-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-actionwhich has a major version update available.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
.github/workflows/ci.yml.github/workflows/docker-publish.ymlAttackMapServer.pyDataServer.pytests/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.pyAttackMapServer.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.pyAttackMapServer.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.pyAttackMapServer.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.pyAttackMapServer.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.pyAttackMapServer.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.pyAttackMapServer.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.pyAttackMapServer.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 to64299(aligning with docker-compose.yml) and update the documentation accordingly, or if1234should 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.utcinstead ofdatetime.UTCimproves 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.
| - name: Install cosign | ||
| if: github.event_name != 'pull_request' | ||
| uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0 | ||
| with: | ||
| cosign-release: 'v2.2.4' |
There was a problem hiding this comment.
🧩 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Signed-off-by: KhulnaSoft bot <43526132+khulnasoft-bot@users.noreply.github.com>
Summary by Sourcery
Enhance attack monitoring with interactive 3D visualization, threat intelligence feedback, historical playback, and automated containerized delivery.
New Features:
Bug Fixes:
Enhancements:
Build:
CI:
Tests:
Summary by CodeRabbit
New Features
Refactor
Chores
Tests