generated from Labyricorn/labyricorn-project-template
Implement CyberSim-OS Phase 1 MVP simulation environment
This commit is contained in:
@@ -2,6 +2,6 @@ _model: devlog
|
||||
---
|
||||
schema_version: 1
|
||||
---
|
||||
title: Labyricorn Project Template development log
|
||||
title: CyberSim-OS development log
|
||||
---
|
||||
summary: Milestones and development log entries for the Labyricorn project template.
|
||||
summary: Milestones and development log entries for the CyberSim-OS simulation platform.
|
||||
@@ -2,13 +2,13 @@ _model: project
|
||||
---
|
||||
schema_version: 1
|
||||
---
|
||||
project_id: labyricorn-project-template
|
||||
project_id: cybersim-os
|
||||
---
|
||||
title: Labyricorn Project Template
|
||||
title: CyberSim-OS
|
||||
---
|
||||
summary:
|
||||
|
||||
Initial template and publishing structure for Labyricorn-compatible projects and development logs.
|
||||
Scriptable CS End User Teaching Environment with Certificate Verification.
|
||||
---
|
||||
status: active
|
||||
---
|
||||
@@ -16,12 +16,12 @@ started: 2026-08-23
|
||||
---
|
||||
author: Labyricorn
|
||||
---
|
||||
repository_url: https://git.labyricorn.com/Labyricorn/labyricorn-project-template
|
||||
repository_url: https://git.labyricorn.com/Labyricorn/CyberSim-OS
|
||||
---
|
||||
default_branch: main
|
||||
---
|
||||
tags: template, lektor, python
|
||||
tags: simulation, cybersecurity, training, offline-first, static-first, education, certificates
|
||||
---
|
||||
body:
|
||||
|
||||
Starter template providing the standard .labyricorn/ publishing records, assistant instructions, devlog structure, and repository tooling for publication on Labyricorn.
|
||||
CyberSim-OS is an offline-first, browser-native cybersecurity simulation environment designed for ordinary end users. Learners navigate a convincing fictional enterprise desktop, perform routine tasks, investigate legitimate and malicious events (phishing, social engineering, malicious attachments), make meaningful decisions, receive multi-axis behavioral after-action reports, and earn cryptographically verifiable completion certificates.
|
||||
@@ -1,213 +1,128 @@
|
||||
# Labyricorn Project Template
|
||||
|
||||
A starter template and baseline structure for projects compatible with
|
||||
[Labyricorn](https://www.labyricorn.com).
|
||||
|
||||
This template provides the required `.labyricorn/` publishing records, assistant
|
||||
guidance files (`AGENTS.md`), devlog structure, and the standalone
|
||||
`devlog_editor.py` management utility for local validation and GUI editing.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Projects published on Labyricorn maintain their own exhibition pages and
|
||||
development logs directly inside their Git repositories under the `.labyricorn/`
|
||||
directory. The Labyricorn site importer periodically synchronizes these records
|
||||
as read-only content.
|
||||
|
||||
This template includes:
|
||||
|
||||
- **`.labyricorn/` publishing structure**: Validated Lektor-format records for
|
||||
project identity and development logs.
|
||||
- **`devlog_editor.py`**: Standalone GUI and CLI tool for managing devlog entries,
|
||||
validating publishing contracts, and safely publishing changes.
|
||||
- **`AGENTS.md` instructions**: Scoped assistant instructions at repository root
|
||||
and publishing directories to ensure AI assistants adhere to the publishing
|
||||
contract.
|
||||
- **`.gitignore`**: Configured exclusions for Python environments, caches, and build
|
||||
artifacts.
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```text
|
||||
.
|
||||
├── .gitignore
|
||||
├── .labyricorn/
|
||||
│ ├── AGENTS.md # Assistant instructions for publishing content
|
||||
│ ├── README.md # Publishing structure documentation
|
||||
│ ├── project/
|
||||
│ │ ├── AGENTS.md # Instructions for the project exhibition record
|
||||
│ │ ├── contents.lr # Project identity and exhibition narrative
|
||||
│ │ └── <optional logo> # Project logo (PNG, JPEG, WebP)
|
||||
│ └── devlog/
|
||||
│ ├── AGENTS.md # Instructions for devlog records
|
||||
│ ├── contents.lr # Devlog index record
|
||||
│ └── <entry-slug>/ # (Optional) Devlog entry directories
|
||||
│ ├── contents.lr # Devlog entry record
|
||||
│ └── <optional img> # Entry attachments (PNG, JPEG, WebP)
|
||||
├── AGENTS.md # Assistant instructions for the repository
|
||||
├── devlog_editor.py # Standalone devlog editor & validator
|
||||
├── LICENSE # License file
|
||||
└── README.md # Project documentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
### 1. Configure the Project Identity
|
||||
|
||||
Edit `.labyricorn/project/contents.lr` to set your project details:
|
||||
|
||||
```ini
|
||||
_model: project
|
||||
---
|
||||
schema_version: 1
|
||||
---
|
||||
project_id: my-project-id
|
||||
---
|
||||
title: My Project Title
|
||||
---
|
||||
summary:
|
||||
|
||||
A concise summary of the project for listings and cards.
|
||||
---
|
||||
status: active
|
||||
---
|
||||
started: 2026-08-23
|
||||
---
|
||||
author: Author Name
|
||||
---
|
||||
repository_url: https://git.labyricorn.com/Owner/my-project
|
||||
---
|
||||
default_branch: main
|
||||
---
|
||||
tags: python, web, tool
|
||||
---
|
||||
body:
|
||||
|
||||
An exhibition narrative describing the project background, goals, and features.
|
||||
```
|
||||
|
||||
#### Project Fields:
|
||||
- `project_id`: Stable lowercase hyphenated slug (e.g. `my-project`).
|
||||
- `title`: Display title for the project.
|
||||
- `summary`: Short multi-line summary.
|
||||
- `status`: One of `active`, `released`, `maintained`, or `archived`.
|
||||
- `started`: Start date in `YYYY-MM-DD` format.
|
||||
- `author`: Author or organization name.
|
||||
- `repository_url`: Public credential-free HTTPS Git URL.
|
||||
- `default_branch`: Target branch name (e.g. `main`).
|
||||
- `logo`: (Optional) Local filename of an image inside `.labyricorn/project/`.
|
||||
- `tags`: Comma-separated technology or topic labels.
|
||||
- `body`: Markdown narrative of the project.
|
||||
|
||||
### 2. Configure the Devlog Index
|
||||
|
||||
Edit `.labyricorn/devlog/contents.lr` to customize the devlog title and summary:
|
||||
|
||||
```ini
|
||||
_model: devlog
|
||||
---
|
||||
schema_version: 1
|
||||
---
|
||||
title: My Project development log
|
||||
---
|
||||
summary: Milestones and development updates for My Project.
|
||||
```
|
||||
|
||||
### 3. Add Devlog Entries
|
||||
|
||||
To add an entry manually, create a directory under `.labyricorn/devlog/<entry-slug>/`
|
||||
containing `contents.lr`:
|
||||
|
||||
```ini
|
||||
_model: devlog-entry
|
||||
---
|
||||
schema_version: 1
|
||||
---
|
||||
title: Initial Project Milestone
|
||||
---
|
||||
date: 2026-08-23
|
||||
---
|
||||
author: Author Name
|
||||
---
|
||||
summary:
|
||||
|
||||
Overview of the milestone accomplishments.
|
||||
---
|
||||
tags: release, setup
|
||||
---
|
||||
source_commit: <40-character-commit-hash>
|
||||
---
|
||||
body:
|
||||
|
||||
Detailed Markdown description of the milestone, technical decisions, and progress.
|
||||
```
|
||||
|
||||
Alternatively, use `devlog_editor.py` to create and edit entries interactively.
|
||||
|
||||
---
|
||||
|
||||
## Devlog Editor
|
||||
|
||||
The included `devlog_editor.py` script is a standalone utility with zero external
|
||||
dependencies (uses Python standard library and Tkinter).
|
||||
|
||||
### CLI Validation
|
||||
|
||||
Validate all project and devlog records without opening a GUI:
|
||||
|
||||
```bash
|
||||
python devlog_editor.py --validate
|
||||
```
|
||||
|
||||
### Graphical Editor
|
||||
|
||||
Launch the Tkinter management interface to view repository status, draft entries,
|
||||
and publish updates:
|
||||
|
||||
```bash
|
||||
python devlog_editor.py
|
||||
```
|
||||
|
||||
Features of the GUI editor:
|
||||
- **Repository status**: Displays current branch, remote, and ahead/behind counts.
|
||||
- **Entry manager**: Create, edit, and delete devlog entries with automatic slug and date handling.
|
||||
- **Publishing safety**: Stages and commits only `.labyricorn/` publishing files, leaving application source files untouched.
|
||||
|
||||
---
|
||||
|
||||
## Publishing Rules & Constraints
|
||||
|
||||
The Labyricorn site importer enforces strict validation rules on imported repositories:
|
||||
|
||||
1. **Native Lektor Records**: Records must be named `contents.lr` and declare `schema_version: 1`.
|
||||
2. **No Raw HTML**: HTML tags (`<script>`, `<style>`, `<div>`, etc.) are prohibited in text fields and Markdown bodies.
|
||||
3. **Approved Images**: Attachments must be PNG, JPEG, or WebP files located within the record directory. Files must match their magic-byte signatures.
|
||||
4. **Limits**: Up to 100 imported files, maximum 5 MiB per file, and maximum 20 MiB total per project snapshot.
|
||||
5. **Source Commits**: Devlog `source_commit` values must be valid 40-character commit hashes existing on the branch history.
|
||||
6. **No Symlinks or Executables**: Linked paths and executable scripts under `.labyricorn/` are rejected.
|
||||
|
||||
---
|
||||
|
||||
## Assistant Guidance (`AGENTS.md`)
|
||||
|
||||
This repository uses hierarchical `AGENTS.md` files to guide coding assistants:
|
||||
|
||||
- **`AGENTS.md` (Root)**: General project guidelines, authorization boundaries, and devlog practices.
|
||||
- **`.labyricorn/AGENTS.md`**: Publishing boundary contract and security rules.
|
||||
- **`.labyricorn/project/AGENTS.md`**: Rules for editing project exhibition metadata.
|
||||
- **`.labyricorn/devlog/AGENTS.md`**: Standards for crafting verifiable devlog entries.
|
||||
|
||||
Coding assistants working in this repository should always review the nearest
|
||||
applicable `AGENTS.md` before making changes.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This template is released into the public domain under the [Unlicense](LICENSE).
|
||||
# CyberSim OS
|
||||
|
||||
**Phase 1 MVP: Scriptable CS End User Teaching Environment with Certificate Verification**
|
||||
|
||||
CyberSim OS is an offline-first, browser-native cybersecurity simulation platform designed for ordinary end users. Learners enter a convincing fictional enterprise desktop, perform routine workplace activities, investigate ambiguous events, encounter realistic threats (credential phishing, malicious attachments) and legitimate false flags, receive a 7-axis behavioral after-action assessment, and earn cryptographically verifiable completion certificates (`*.cybercert`).
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Convincing Fictional Desktop**: Original Windows-like theme with draggable/resizable windows, active/inactive focus states, taskbar, Start launcher, simulation clock, and system toast notifications with audio cues.
|
||||
- **Core Simulated Workplace Applications**:
|
||||
- **Inlook**: Email client with header inspector (RFC sender vs. friendly name), link hover destination tooltips, phishing report workflow, and reply/delete actions.
|
||||
- **Navigator**: Web browser supporting intranet directories, policy hubs, and external spoofed credential-harvesting portals.
|
||||
- **Files**: Virtual filesystem managing Documents, Downloads, and Company Shared folders.
|
||||
- **Doc Viewer**: Lightweight renderer for spreadsheets (`.xlsx`), policies (`.pdf`), and memos.
|
||||
- **Security Center**: Endpoint protection dashboard, real-time alert logs, incident report confirmations, and delayed alert triggers.
|
||||
- **Realistic Decision Model**: Teaches *"Observe → Investigate → Verify → Decide → Act"* rather than *"Strange = Malicious"*. False flags and legitimate urgent notices test discernment.
|
||||
- **Delayed Consequences Engine**: Unsafe actions (e.g. submitting credentials on a phishing page) trigger delayed consequence alerts in Security Center without giving immediate arcade-like game-over feedback.
|
||||
- **7-Axis Behavioral Scoring**:
|
||||
1. *Threat Detection* (20 pts)
|
||||
2. *Investigation & Evidence Gathering* (20 pts)
|
||||
3. *Safe Handling* (15 pts)
|
||||
4. *Independent Verification* (15 pts)
|
||||
5. *Incident Reporting* (15 pts)
|
||||
6. *False Positive Control* (15 pts)
|
||||
7. *Operational Judgment* (Passing threshold: 80 / 100)
|
||||
- **Verifiable Cryptographic Certificates**:
|
||||
- Web Crypto SHA-256 scenario fingerprinting.
|
||||
- Portable, structured `*.cybercert` JSON credential export.
|
||||
- Standalone offline certificate validator (`verify.html`).
|
||||
- **100% Offline-First & Zero Dependencies**: Runs directly from any static web server, GitHub Pages, or the bundled `launcher.py` with zero npm/node/external CDN requirements.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Option 1: Standalone Local Launcher (Python 3)
|
||||
Run the lightweight local launcher to start the server at `http://127.0.0.1:8080`:
|
||||
|
||||
```bash
|
||||
python launcher.py
|
||||
```
|
||||
|
||||
Your default web browser will open automatically.
|
||||
|
||||
### Option 2: Static Web Server
|
||||
Serve the `src/` directory with any static HTTP server:
|
||||
|
||||
```bash
|
||||
cd src
|
||||
python -m http.server 8000
|
||||
```
|
||||
Open [http://127.0.0.1:8000](http://127.0.0.1:8000) in your browser.
|
||||
|
||||
### Option 3: Direct Static Hosting
|
||||
Host the repository on GitHub Pages or any static file host pointing to `src/index.html`.
|
||||
|
||||
---
|
||||
|
||||
## Certificate Verification
|
||||
|
||||
To verify a `*.cybercert` certificate issued by CyberSim OS:
|
||||
|
||||
1. Open `src/verify.html` in your web browser.
|
||||
2. Drag and drop the `*.cybercert` file into the verification area.
|
||||
3. The offline verifier recalculates the SHA-256 integrity hash, verifies the scenario fingerprint, and confirms the passing score and learner identity.
|
||||
|
||||
---
|
||||
|
||||
## Architecture & Directory Structure
|
||||
|
||||
```
|
||||
CyberSim-OS/
|
||||
├── .labyricorn/ # Labyricorn exhibition & devlog publishing records
|
||||
├── src/
|
||||
│ ├── index.html # Main desktop simulation interface
|
||||
│ ├── verify.html # Standalone offline certificate verifier
|
||||
│ ├── css/
|
||||
│ │ ├── theme-windows.css # Fictional Windows-like enterprise theme tokens
|
||||
│ │ ├── desktop.css # Window manager, taskbar, start menu, tray, toasts
|
||||
│ │ ├── components.css # Modals, form inputs, buttons, badges, tabs
|
||||
│ │ └── apps.css # Inlook, Navigator, Files, DocViewer, SecurityCenter
|
||||
│ ├── js/
|
||||
│ │ ├── main.js # Application bootstrapper & scenario loader
|
||||
│ │ ├── core/
|
||||
│ │ │ ├── window_manager.js# Window lifecycle, stacking, drag, min/max/close
|
||||
│ │ │ ├── desktop.js # Desktop shell, launcher, clock, simulation controls
|
||||
│ │ │ └── notifications.js # Toast notifications & Web Audio synth audio chime
|
||||
│ │ ├── apps/
|
||||
│ │ │ ├── inlook.js # Email client with RFC header/link inspection
|
||||
│ │ │ ├── navigator.js # Simulated browser with intranet & phishing pages
|
||||
│ │ │ ├── files.js # Virtual filesystem explorer
|
||||
│ │ │ ├── docviewer.js # Spreadsheet and document viewer
|
||||
│ │ │ └── security_center.js # Endpoint status & incident report dashboard
|
||||
│ │ ├── engine/
|
||||
│ │ │ ├── event_bus.js # Behavioral telemetry logger
|
||||
│ │ │ └── consequence.js # Delayed consequence scheduler
|
||||
│ │ ├── scenario/
|
||||
│ │ │ ├── scenario_ref1.js # Reference scenario ("NexaCore Shift 1")
|
||||
│ │ │ └── fingerprint.js # Web Crypto SHA-256 scenario fingerprinting
|
||||
│ │ ├── scoring/
|
||||
│ │ │ ├── scorer.js # Multi-axis behavioral scoring engine
|
||||
│ │ │ └── aar.js # After-Action Report modal with pedagogical feedback
|
||||
│ │ └── cert/
|
||||
│ │ ├── cert_generator.js# *.cybercert JSON generator & printable certificate
|
||||
│ │ └── cert_verifier.js # Offline cryptographic certificate verifier
|
||||
│ └── assets/ # Embedded SVG icons and media
|
||||
├── launcher.py # Zero-dependency local 127.0.0.1 web server
|
||||
├── devlog_editor.py # Labyricorn devlog validation & editing tool
|
||||
└── README.md # Project documentation
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Validation & Devlog Maintenance
|
||||
|
||||
Run the validation suite to ensure `.labyricorn/` records and devlog entries adhere to the schema:
|
||||
|
||||
```bash
|
||||
python devlog_editor.py --validate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License. See `LICENSE` for details.
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CyberSim OS - Standalone Offline Local Launcher
|
||||
Zero external dependencies. Binds strictly to 127.0.0.1 and opens default browser.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import http.server
|
||||
import socketserver
|
||||
import webbrowser
|
||||
import threading
|
||||
import time
|
||||
|
||||
PORT = 8080
|
||||
HOST = '127.0.0.1'
|
||||
|
||||
class CustomHandler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *args, **kwargs):
|
||||
src_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'src')
|
||||
super().__init__(*args, directory=src_dir, **kwargs)
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
def start_server():
|
||||
server = None
|
||||
for port in range(PORT, PORT + 20):
|
||||
try:
|
||||
server = socketserver.TCPServer((HOST, port), CustomHandler)
|
||||
print(f"============================================================")
|
||||
print(f" CyberSim OS - Offline Simulation Environment")
|
||||
print(f" Running locally at: http://{HOST}:{port}")
|
||||
print(f" Press Ctrl+C to stop the launcher")
|
||||
print(f"============================================================")
|
||||
|
||||
def open_browser():
|
||||
time.sleep(0.6)
|
||||
webbrowser.open(f"http://{HOST}:{port}")
|
||||
|
||||
threading.Thread(target=open_browser, daemon=True).start()
|
||||
server.serve_forever()
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
start_server()
|
||||
except KeyboardInterrupt:
|
||||
print("\nCyberSim OS stopped.")
|
||||
sys.exit(0)
|
||||
@@ -0,0 +1,871 @@
|
||||
/* CyberSim OS - App Specific Styles */
|
||||
|
||||
/* === INLOOK (Email Client) === */
|
||||
.inlook-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.inlook-sidebar {
|
||||
width: 160px;
|
||||
background: #f1f5f9;
|
||||
border-right: 1px solid var(--border-medium);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px 6px;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inlook-folder-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
color: #334155;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.inlook-folder-item:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.inlook-folder-item.active {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.inlook-list-pane {
|
||||
width: 280px;
|
||||
border-right: 1px solid var(--border-medium);
|
||||
background: #ffffff;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.inlook-list-item {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
cursor: pointer;
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
|
||||
.inlook-list-item:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.inlook-list-item.active {
|
||||
background: #eff6ff;
|
||||
border-left: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.inlook-list-item.unread .inlook-item-sender,
|
||||
.inlook-list-item.unread .inlook-item-subject {
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.inlook-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 11px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.inlook-item-sender {
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.inlook-item-date {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.inlook-item-subject {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.inlook-item-snippet {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.inlook-reading-pane {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #ffffff;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.inlook-toolbar {
|
||||
padding: 8px 12px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--border-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inlook-msg-header {
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.inlook-msg-subject {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.inlook-msg-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.inlook-sender-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.inlook-sender-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.inlook-sender-avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--radius-full);
|
||||
background: #e2e8f0;
|
||||
color: #475569;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.inlook-sender-details .sender-name {
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.inlook-sender-details .sender-email {
|
||||
color: #64748b;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
.inlook-inspect-btn {
|
||||
font-size: 10px;
|
||||
color: var(--color-primary);
|
||||
background: transparent;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 2px 6px;
|
||||
cursor: pointer;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.inlook-inspect-btn:hover {
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
|
||||
.inlook-attachments {
|
||||
margin-top: 10px;
|
||||
padding-top: 10px;
|
||||
border-top: 1px dashed var(--border-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.inlook-attachment-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
background: #f1f5f9;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s ease;
|
||||
}
|
||||
|
||||
.inlook-attachment-chip:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.inlook-msg-body {
|
||||
padding: 18px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #1e293b;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.inlook-msg-body p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.inlook-msg-link {
|
||||
color: var(--color-primary);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* === NAVIGATOR (Web Browser) === */
|
||||
.nav-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.nav-toolbar {
|
||||
padding: 6px 10px;
|
||||
background: #f1f5f9;
|
||||
border-bottom: 1px solid var(--border-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-btn-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #475569;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-btn-icon:hover {
|
||||
background: #e2e8f0;
|
||||
border-color: #cbd5e1;
|
||||
}
|
||||
|
||||
.nav-address-bar {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--radius-full);
|
||||
padding: 3px 12px;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nav-address-bar.secure {
|
||||
border-color: #10b981;
|
||||
}
|
||||
|
||||
.nav-address-bar.insecure {
|
||||
border-color: #f59e0b;
|
||||
}
|
||||
|
||||
.nav-lock-icon {
|
||||
color: #10b981;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-url-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: #0f172a;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.nav-viewport {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: #f8fafc;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
/* Simulated Web Pages */
|
||||
.webpage-intranet {
|
||||
padding: 24px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.intranet-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 2px solid #2563eb;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.intranet-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1e3a8a;
|
||||
}
|
||||
|
||||
.intranet-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.intranet-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 16px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.intranet-card h3 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.directory-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.directory-table th, .directory-table td {
|
||||
padding: 6px 8px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
}
|
||||
|
||||
.directory-table th {
|
||||
background: #f1f5f9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Phishing Web Page */
|
||||
.webpage-phish {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100%;
|
||||
padding: 30px;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
.phish-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 28px;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.phish-logo {
|
||||
font-size: 18px;
|
||||
font-weight: 800;
|
||||
color: #2563eb;
|
||||
margin-bottom: 6px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.phish-subtitle {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* === FILES (Virtual File Explorer) === */
|
||||
.files-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.files-sidebar {
|
||||
width: 180px;
|
||||
background: #f8fafc;
|
||||
border-right: 1px solid var(--border-medium);
|
||||
padding: 12px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.files-folder-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
color: #334155;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.files-folder-btn:hover {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.files-folder-btn.active {
|
||||
background: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.files-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.files-address-bar {
|
||||
padding: 8px 12px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid var(--border-medium);
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.files-grid {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
|
||||
gap: 12px;
|
||||
overflow-y: auto;
|
||||
align-content: flex-start;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 10px 6px;
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: all 0.12s ease;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: var(--border-medium);
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
background: var(--color-primary-light);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 11px;
|
||||
color: #0f172a;
|
||||
word-break: break-word;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.file-size {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* === DOCUMENT VIEWER === */
|
||||
.doc-viewer-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.doc-toolbar {
|
||||
padding: 6px 12px;
|
||||
background: #0f172a;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.doc-title-info {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.doc-canvas {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.doc-page {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
padding: 32px 40px;
|
||||
max-width: 720px;
|
||||
width: 100%;
|
||||
min-height: 800px;
|
||||
color: #0f172a;
|
||||
user-select: text;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.doc-page h1 {
|
||||
font-size: 20px;
|
||||
color: #1e3a8a;
|
||||
border-bottom: 2px solid #2563eb;
|
||||
padding-bottom: 6px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.doc-page h2 {
|
||||
font-size: 15px;
|
||||
color: #0f172a;
|
||||
margin: 16px 0 8px 0;
|
||||
}
|
||||
|
||||
.doc-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 14px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.doc-table th, .doc-table td {
|
||||
border: 1px solid #cbd5e1;
|
||||
padding: 8px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.doc-table th {
|
||||
background: #f1f5f9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* === SECURITY CENTER === */
|
||||
.sec-center-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.sec-sidebar {
|
||||
width: 180px;
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
padding: 14px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sec-nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.12s ease;
|
||||
}
|
||||
|
||||
.sec-nav-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.sec-nav-btn.active {
|
||||
background: #2563eb;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sec-main {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sec-header {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.sec-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.sec-status-banner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 18px;
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 18px;
|
||||
background: #dcfce7;
|
||||
border: 1px solid #86efac;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.sec-status-banner.alert-state {
|
||||
background: #fee2e2;
|
||||
border-color: #fca5a5;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.sec-alert-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sec-alert-item {
|
||||
background: #ffffff;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.sec-alert-item.high {
|
||||
border-left: 4px solid var(--color-danger);
|
||||
}
|
||||
|
||||
.sec-alert-item.medium {
|
||||
border-left: 4px solid var(--color-warning);
|
||||
}
|
||||
|
||||
.sec-alert-item.info {
|
||||
border-left: 4px solid var(--color-info);
|
||||
}
|
||||
|
||||
/* === AFTER-ACTION REPORT (AAR) === */
|
||||
.aar-modal-content {
|
||||
padding: 24px;
|
||||
max-width: 680px;
|
||||
}
|
||||
|
||||
.aar-header-summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius-lg);
|
||||
background: #f1f5f9;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.aar-score-badge {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.aar-result-pill {
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--radius-full);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.aar-result-pill.pass {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
border: 1px solid #86efac;
|
||||
}
|
||||
|
||||
.aar-result-pill.fail {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fca5a5;
|
||||
}
|
||||
|
||||
.aar-category-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.aar-cat-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.aar-progress-bg {
|
||||
height: 8px;
|
||||
background: #e2e8f0;
|
||||
border-radius: var(--radius-full);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.aar-progress-fill {
|
||||
height: 100%;
|
||||
background: #2563eb;
|
||||
border-radius: var(--radius-full);
|
||||
transition: width 0.4s ease;
|
||||
}
|
||||
|
||||
.aar-progress-fill.danger {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.aar-progress-fill.warning {
|
||||
background: #d97706;
|
||||
}
|
||||
|
||||
.aar-progress-fill.success {
|
||||
background: #16a34a;
|
||||
}
|
||||
|
||||
.aar-feedback-box {
|
||||
margin-top: 18px;
|
||||
padding: 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
/* === PRINTABLE CERTIFICATE === */
|
||||
.cert-printable {
|
||||
border: 8px double #1e3a8a;
|
||||
padding: 30px;
|
||||
background: #fffdfa;
|
||||
text-align: center;
|
||||
color: #0f172a;
|
||||
position: relative;
|
||||
margin: 10px auto;
|
||||
max-width: 600px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.cert-org {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.15em;
|
||||
color: #475569;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.cert-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
color: #1e3a8a;
|
||||
margin: 12px 0 6px 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.cert-subtitle {
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.cert-recipient {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
border-bottom: 2px solid #cbd5e1;
|
||||
display: inline-block;
|
||||
padding: 0 24px 4px 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.cert-text {
|
||||
font-size: 12px;
|
||||
color: #475569;
|
||||
max-width: 480px;
|
||||
margin: 0 auto 18px auto;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.cert-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
font-size: 10px;
|
||||
color: #64748b;
|
||||
text-align: left;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
padding-top: 12px;
|
||||
margin-top: 12px;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
/* CyberSim OS - Reusable Components */
|
||||
.cs-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border-medium);
|
||||
background: #ffffff;
|
||||
color: var(--text-main);
|
||||
cursor: pointer;
|
||||
transition: all 0.12s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cs-btn:hover {
|
||||
background: #f1f5f9;
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
|
||||
.cs-btn:active {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
|
||||
.cs-btn-primary {
|
||||
background: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cs-btn-primary:hover {
|
||||
background: var(--color-primary-hover);
|
||||
border-color: var(--color-primary-hover);
|
||||
}
|
||||
|
||||
.cs-btn-danger {
|
||||
background: var(--color-danger);
|
||||
border-color: var(--color-danger);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cs-btn-danger:hover {
|
||||
background: #b91c1c;
|
||||
border-color: #b91c1c;
|
||||
}
|
||||
|
||||
.cs-btn-warning {
|
||||
background: var(--color-warning);
|
||||
border-color: var(--color-warning);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cs-btn-warning:hover {
|
||||
background: #b45309;
|
||||
}
|
||||
|
||||
.cs-btn-sm {
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.cs-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-full);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cs-badge-danger {
|
||||
background: var(--color-danger-bg);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.cs-badge-warning {
|
||||
background: var(--color-warning-bg);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.cs-badge-success {
|
||||
background: var(--color-success-bg);
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.cs-badge-info {
|
||||
background: var(--color-info-bg);
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
/* Modal Overlay */
|
||||
.cs-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 5000;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.cs-modal {
|
||||
background: #ffffff;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 20px 48px rgba(0, 0, 0, 0.4);
|
||||
max-width: 560px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: modalPop 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes modalPop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.cs-modal-header {
|
||||
padding: 14px 18px;
|
||||
background: #0f172a;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.cs-modal-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cs-modal-body {
|
||||
padding: 18px;
|
||||
overflow-y: auto;
|
||||
max-height: 70vh;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.cs-modal-footer {
|
||||
padding: 12px 18px;
|
||||
background: #f8fafc;
|
||||
border-top: 1px solid var(--border-medium);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.cs-form-group {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.cs-form-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.cs-input, .cs-select, .cs-textarea {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
font-size: 12px;
|
||||
font-family: inherit;
|
||||
border: 1px solid var(--border-medium);
|
||||
border-radius: var(--radius-sm);
|
||||
background: #ffffff;
|
||||
color: var(--text-main);
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.cs-input:focus, .cs-select:focus, .cs-textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 2px var(--color-primary-light);
|
||||
}
|
||||
|
||||
/* Tooltips */
|
||||
.cs-tooltip-target {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.cs-link-inspector-tooltip {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
left: 0;
|
||||
background: #0f172a;
|
||||
color: #ffffff;
|
||||
padding: 6px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
z-index: 100;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
border: 1px solid #334155;
|
||||
}
|
||||
|
||||
.cs-link-inspector-tooltip .label {
|
||||
color: #94a3b8;
|
||||
font-size: 10px;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
font-family: var(--font-system);
|
||||
}
|
||||
@@ -0,0 +1,553 @@
|
||||
/* CyberSim OS - Desktop Shell, Windows, Taskbar, Start Menu, Toasts */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
body, html {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: var(--font-system);
|
||||
font-size: 13px;
|
||||
color: var(--text-main);
|
||||
background: var(--bg-desktop);
|
||||
}
|
||||
|
||||
/* Desktop Area */
|
||||
#desktop {
|
||||
position: relative;
|
||||
width: 100vw;
|
||||
height: calc(100vh - var(--taskbar-height));
|
||||
overflow: hidden;
|
||||
background: radial-gradient(circle at 75% 25%, #1e3a5f 0%, #0f172a 65%, #080c14 100%);
|
||||
}
|
||||
|
||||
#desktop::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-image: radial-gradient(rgba(255, 255, 255, 0.05) 1px, transparent 0);
|
||||
background-size: 28px 28px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Desktop Icons */
|
||||
.desktop-icons {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
left: 14px;
|
||||
bottom: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-content: flex-start;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.desktop-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 82px;
|
||||
height: 84px;
|
||||
padding: 6px 4px;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
|
||||
transition: all 0.12s ease;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.desktop-icon:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.desktop-icon.selected {
|
||||
background: rgba(37, 99, 235, 0.35);
|
||||
border-color: rgba(96, 165, 250, 0.6);
|
||||
}
|
||||
|
||||
.desktop-icon-img {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.desktop-icon-img svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.desktop-icon-label {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
word-break: break-word;
|
||||
max-width: 76px;
|
||||
}
|
||||
|
||||
/* Window Component */
|
||||
.cs-window {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-window);
|
||||
border: 1px solid var(--border-window);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-window);
|
||||
overflow: hidden;
|
||||
min-width: 440px;
|
||||
min-height: 300px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.cs-window.active {
|
||||
border-color: var(--border-window-active);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.cs-window.minimized {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cs-window.maximized {
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: calc(100vh - var(--taskbar-height)) !important;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.cs-window-header {
|
||||
height: 36px;
|
||||
background: var(--bg-window-header-inactive);
|
||||
color: var(--text-window-header-inactive);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.15);
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.cs-window.active .cs-window-header {
|
||||
background: var(--bg-window-header);
|
||||
color: var(--text-window-header-active);
|
||||
}
|
||||
|
||||
.cs-window-header:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.cs-window-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.cs-window-title svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cs-window-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.cs-btn-win {
|
||||
width: 26px;
|
||||
height: 22px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
|
||||
.cs-btn-win:hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.cs-btn-win.close:hover {
|
||||
background: var(--color-danger);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.cs-window-body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Taskbar */
|
||||
#taskbar {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: var(--taskbar-height);
|
||||
background: var(--bg-taskbar);
|
||||
backdrop-filter: blur(16px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 6px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.taskbar-left, .taskbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.taskbar-apps {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.taskbar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--bg-taskbar-item);
|
||||
color: #e2e8f0;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.taskbar-btn:hover {
|
||||
background: var(--bg-taskbar-item-hover);
|
||||
}
|
||||
|
||||
.taskbar-btn.active {
|
||||
background: var(--bg-taskbar-item-active);
|
||||
border-color: rgba(59, 130, 246, 0.6);
|
||||
color: #ffffff;
|
||||
border-bottom: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.taskbar-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.taskbar-btn.start-btn {
|
||||
background: linear-gradient(135deg, #1d4ed8 0%, #2563eb 100%);
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.taskbar-btn.start-btn:hover {
|
||||
background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
|
||||
}
|
||||
|
||||
.taskbar-clock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
padding: 0 8px;
|
||||
height: 34px;
|
||||
border-radius: var(--radius-md);
|
||||
color: #e2e8f0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.taskbar-clock:hover {
|
||||
background: var(--bg-taskbar-item-hover);
|
||||
}
|
||||
|
||||
.taskbar-clock .time {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.taskbar-clock .date {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.taskbar-tray-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 34px;
|
||||
border-radius: var(--radius-md);
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.taskbar-tray-icon:hover {
|
||||
background: var(--bg-taskbar-item-hover);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.taskbar-tray-icon .badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 5px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background: var(--color-danger);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Start Menu */
|
||||
#start-menu {
|
||||
position: absolute;
|
||||
bottom: calc(var(--taskbar-height) + 6px);
|
||||
left: 6px;
|
||||
width: 360px;
|
||||
max-height: 520px;
|
||||
background: var(--bg-start-menu);
|
||||
border: 1px solid var(--border-start-menu);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.65);
|
||||
z-index: 1050;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(20px);
|
||||
}
|
||||
|
||||
#start-menu.open {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.start-header {
|
||||
padding: 14px 16px;
|
||||
background: rgba(15, 23, 42, 0.7);
|
||||
border-bottom: 1px solid var(--border-start-menu);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.start-user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-full);
|
||||
background: linear-gradient(135deg, #2563eb 0%, #3b82f6 100%);
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 700;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.start-user-info .user-name {
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.start-user-info .user-role {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.start-body {
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.start-section-title {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: #64748b;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.start-app-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.start-app-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-md);
|
||||
color: #e2e8f0;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s ease;
|
||||
}
|
||||
|
||||
.start-app-item:hover {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.start-app-item svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.start-footer {
|
||||
padding: 10px 14px;
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
border-top: 1px solid var(--border-start-menu);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.start-btn-finish {
|
||||
background: #dc2626;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
padding: 7px 12px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.start-btn-finish:hover {
|
||||
background: #b91c1c;
|
||||
}
|
||||
|
||||
/* Toast Notifications Container */
|
||||
#notification-container {
|
||||
position: absolute;
|
||||
bottom: calc(var(--taskbar-height) + 12px);
|
||||
right: 12px;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
gap: 8px;
|
||||
z-index: 2000;
|
||||
pointer-events: none;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.cs-toast {
|
||||
pointer-events: auto;
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
border: 1px solid #334155;
|
||||
border-left: 4px solid var(--color-primary);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 10px 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
animation: toastIn 0.25s ease-out;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.cs-toast.warning {
|
||||
border-left-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.cs-toast.danger {
|
||||
border-left-color: var(--color-danger);
|
||||
}
|
||||
|
||||
.cs-toast.success {
|
||||
border-left-color: var(--color-success);
|
||||
}
|
||||
|
||||
@keyframes toastIn {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cs-toast-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.cs-toast-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.cs-toast-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.cs-toast-body {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.cs-toast-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.cs-toast-close:hover {
|
||||
color: #ffffff;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/* CyberSim OS - Theme Windows (Original Enterprise Theme) */
|
||||
:root {
|
||||
--bg-desktop: #1a2332;
|
||||
--bg-taskbar: rgba(15, 23, 42, 0.95);
|
||||
--bg-taskbar-item: rgba(255, 255, 255, 0.08);
|
||||
--bg-taskbar-item-active: rgba(255, 255, 255, 0.2);
|
||||
--bg-taskbar-item-hover: rgba(255, 255, 255, 0.12);
|
||||
--bg-start-menu: #1e293b;
|
||||
--border-start-menu: #334155;
|
||||
|
||||
--bg-window: #ffffff;
|
||||
--bg-window-header: #0f172a;
|
||||
--bg-window-header-inactive: #334155;
|
||||
--text-window-header-active: #ffffff;
|
||||
--text-window-header-inactive: #94a3b8;
|
||||
--border-window: #94a3b8;
|
||||
--border-window-active: #3b82f6;
|
||||
--shadow-window: 0 12px 32px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||
|
||||
--color-primary: #2563eb;
|
||||
--color-primary-hover: #1d4ed8;
|
||||
--color-primary-light: #eff6ff;
|
||||
--color-success: #16a34a;
|
||||
--color-success-bg: #dcfce7;
|
||||
--color-warning: #d97706;
|
||||
--color-warning-bg: #fef3c7;
|
||||
--color-danger: #dc2626;
|
||||
--color-danger-bg: #fee2e2;
|
||||
--color-info: #0284c7;
|
||||
--color-info-bg: #e0f2fe;
|
||||
|
||||
--text-main: #0f172a;
|
||||
--text-muted: #64748b;
|
||||
--text-inverse: #ffffff;
|
||||
--border-subtle: #f1f5f9;
|
||||
--border-medium: #cbd5e1;
|
||||
|
||||
--font-system: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
|
||||
--radius-sm: 4px;
|
||||
--radius-md: 6px;
|
||||
--radius-lg: 10px;
|
||||
--radius-full: 9999px;
|
||||
|
||||
--taskbar-height: 44px;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CyberSim OS - Enterprise Simulation Environment</title>
|
||||
<link rel="stylesheet" href="css/theme-windows.css">
|
||||
<link rel="stylesheet" href="css/desktop.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/apps.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- Desktop Surface -->
|
||||
<div id="desktop">
|
||||
<div class="desktop-icons" id="desktop-icons"></div>
|
||||
<div id="notification-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Start Menu -->
|
||||
<div id="start-menu">
|
||||
<div class="start-header">
|
||||
<div class="start-user-avatar">JT</div>
|
||||
<div class="start-user-info">
|
||||
<div class="user-name">Jordan Taylor</div>
|
||||
<div class="user-role">Financial Operations • NexaCore</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="start-body">
|
||||
<div class="start-section-title">Shift Responsibilities</div>
|
||||
<div style="background:rgba(255,255,255,0.05); padding:10px; border-radius:6px; font-size:11px; color:#cbd5e1; line-height:1.4;">
|
||||
• Review Inlook communications & tasks<br>
|
||||
• Inspect Q3 Budget Forecast in Documents<br>
|
||||
• Review NexaCore Cybersecurity Policy v4<br>
|
||||
• Report suspicious incidents to Security Center
|
||||
</div>
|
||||
|
||||
<div class="start-section-title" style="margin-top:6px;">Applications</div>
|
||||
<div class="start-app-list" id="start-app-list"></div>
|
||||
</div>
|
||||
|
||||
<div class="start-footer">
|
||||
<div style="font-size:10px; color:#64748b; font-family:var(--font-mono);">CyberSim v1.0.0 (Phase 1 MVP)</div>
|
||||
<button class="start-btn-finish" id="start-btn-finish">Finish Shift & View Assessment</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Taskbar -->
|
||||
<div id="taskbar">
|
||||
<div class="taskbar-left">
|
||||
<button class="taskbar-btn start-btn" id="start-btn">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><rect x="3" y="3" width="8" height="8" rx="1"/><rect x="13" y="3" width="8" height="8" rx="1"/><rect x="3" y="13" width="8" height="8" rx="1"/><rect x="13" y="13" width="8" height="8" rx="1"/></svg>
|
||||
<span>NexaCore</span>
|
||||
</button>
|
||||
<div class="taskbar-apps" id="taskbar-apps"></div>
|
||||
</div>
|
||||
|
||||
<div class="taskbar-right">
|
||||
<div class="taskbar-tray-icon" title="Network: NexaCore-Corporate-WPA3 (Protected)">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M1.42 9a16 16 0 0 1 21.16 0"/><path d="M8.53 16.11a6 6 0 0 1 6.95 0"/><line x1="12" y1="20" x2="12.01" y2="20"/></svg>
|
||||
</div>
|
||||
<div class="taskbar-tray-icon" id="tray-notify-icon" title="Security & Notifications">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>
|
||||
<span class="badge" id="tray-notify-badge" style="display:none;"></span>
|
||||
</div>
|
||||
<div class="taskbar-clock" id="taskbar-clock"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* CyberSim OS - Document & Spreadsheet Viewer
|
||||
*/
|
||||
|
||||
export class DocViewerApp {
|
||||
constructor({ windowManager, eventBus }) {
|
||||
this.wm = windowManager;
|
||||
this.eventBus = eventBus;
|
||||
}
|
||||
|
||||
getIconSvg() {
|
||||
return `<svg viewBox="0 0 24 24" fill="none" stroke="#16a34a" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>`;
|
||||
}
|
||||
|
||||
openDocument(file) {
|
||||
const win = this.wm.createWindow({
|
||||
id: `doc_${file.id}`,
|
||||
title: `${file.name} - Document Viewer`,
|
||||
iconSvg: this.getIconSvg(),
|
||||
width: 820,
|
||||
height: 600,
|
||||
bodyContent: this.renderDocument(file)
|
||||
});
|
||||
|
||||
this.eventBus.emit('DOC_VIEWED', {
|
||||
target: file.name,
|
||||
details: { fileId: file.id, type: file.type }
|
||||
});
|
||||
}
|
||||
|
||||
renderDocument(file) {
|
||||
if (file.type === 'spreadsheet' && file.content) {
|
||||
return `
|
||||
<div class="doc-viewer-container">
|
||||
<div class="doc-toolbar">
|
||||
<div class="doc-title-info">
|
||||
<span>📊 ${file.name}</span>
|
||||
<span class="cs-badge cs-badge-success">Read-Only</span>
|
||||
</div>
|
||||
<div style="font-size:11px; color:#94a3b8;">Format: XLSX Tabular</div>
|
||||
</div>
|
||||
<div class="doc-canvas">
|
||||
<div class="doc-page">
|
||||
<h1>${file.content.title}</h1>
|
||||
<p style="font-size:12px; color:#64748b; margin-bottom:14px;">Confidential — Internal Use Only — Prepared for NexaCore Leadership</p>
|
||||
<table class="doc-table">
|
||||
<thead>
|
||||
<tr>
|
||||
${file.content.headers.map(h => `<th>${h}</th>`).join('')}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${file.content.rows.map(row => `
|
||||
<tr>
|
||||
${row.map((cell, idx) => `<td style="${idx > 0 ? 'text-align:right;' : 'font-weight:500;'}">${cell}</td>`).join('')}
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top:20px; font-size:11px; color:#64748b;">
|
||||
<strong>Notes:</strong> Operating margin buffer is currently aligned with Q2 audit targets. Prepared by Finance Ops.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (file.type === 'pdf' && file.content) {
|
||||
return `
|
||||
<div class="doc-viewer-container">
|
||||
<div class="doc-toolbar">
|
||||
<div class="doc-title-info">
|
||||
<span>📑 ${file.name}</span>
|
||||
<span class="cs-badge cs-badge-info">NexaCore Document</span>
|
||||
</div>
|
||||
<div style="font-size:11px; color:#94a3b8;">Format: PDF Document</div>
|
||||
</div>
|
||||
<div class="doc-canvas">
|
||||
<div class="doc-page">
|
||||
<h1>${file.content.title}</h1>
|
||||
${file.content.sections ? file.content.sections.map(s => `
|
||||
<h2>${s.heading}</h2>
|
||||
<p>${s.text}</p>
|
||||
`).join('') : ''}
|
||||
|
||||
${file.content.table ? `
|
||||
<table class="doc-table" style="margin-top:16px;">
|
||||
<thead>
|
||||
<tr>
|
||||
${file.content.table.headers.map(h => `<th>${h}</th>`).join('')}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${file.content.table.rows.map(r => `
|
||||
<tr>
|
||||
${r.map(c => `<td>${c}</td>`).join('')}
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
` : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="doc-viewer-container">
|
||||
<div class="doc-toolbar">
|
||||
<div class="doc-title-info">${file.name}</div>
|
||||
</div>
|
||||
<div class="doc-canvas">
|
||||
<div class="doc-page">
|
||||
<h2>Archive Preview</h2>
|
||||
<p>Archive file "${file.name}" contents cannot be executed directly within simulated document viewer.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* CyberSim OS - Files Virtual File Explorer
|
||||
*/
|
||||
|
||||
export class FilesApp {
|
||||
constructor({ windowManager, eventBus, notifications, scenario, onOpenFile }) {
|
||||
this.wm = windowManager;
|
||||
this.eventBus = eventBus;
|
||||
this.notifications = notifications;
|
||||
this.scenario = scenario;
|
||||
this.onOpenFile = onOpenFile;
|
||||
this.currentFolder = 'Documents';
|
||||
this.files = JSON.parse(JSON.stringify(scenario.files));
|
||||
}
|
||||
|
||||
getIconSvg() {
|
||||
return `<svg viewBox="0 0 24 24" fill="none" stroke="#eab308" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
|
||||
}
|
||||
|
||||
launch(initialFolder = 'Documents') {
|
||||
this.currentFolder = initialFolder;
|
||||
const win = this.wm.createWindow({
|
||||
id: 'files',
|
||||
title: 'Files - Corporate Storage',
|
||||
iconSvg: this.getIconSvg(),
|
||||
width: 780,
|
||||
height: 500,
|
||||
bodyContent: this.renderShell()
|
||||
});
|
||||
|
||||
this.bindEvents(win.bodyElement);
|
||||
this.renderFolderContents();
|
||||
this.eventBus.emit('APP_OPENED', { target: 'files' });
|
||||
}
|
||||
|
||||
renderShell() {
|
||||
return `
|
||||
<div class="files-container">
|
||||
<div class="files-sidebar">
|
||||
<div class="files-folder-btn active" data-folder="Documents">
|
||||
<span>[Docs] Documents</span>
|
||||
</div>
|
||||
<div class="files-folder-btn" data-folder="Downloads">
|
||||
<span>[Down] Downloads</span>
|
||||
</div>
|
||||
<div class="files-folder-btn" data-folder="Company Shared">
|
||||
<span>[Share] Company Shared</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="files-content">
|
||||
<div class="files-address-bar" id="files-current-path">Location: /Documents</div>
|
||||
<div class="files-grid" id="files-grid"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
bindEvents(container) {
|
||||
container.querySelectorAll('.files-folder-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
container.querySelectorAll('.files-folder-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
this.currentFolder = btn.dataset.folder;
|
||||
this.renderFolderContents();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
addFile(fileObj) {
|
||||
this.files.push(fileObj);
|
||||
this.renderFolderContents();
|
||||
}
|
||||
|
||||
renderFolderContents() {
|
||||
const pathEl = document.getElementById('files-current-path');
|
||||
const gridEl = document.getElementById('files-grid');
|
||||
if (pathEl) pathEl.innerText = `Location: /${this.currentFolder}`;
|
||||
if (!gridEl) return;
|
||||
|
||||
const filtered = this.files.filter(f => f.folder === this.currentFolder);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
gridEl.innerHTML = `<div style="grid-column:1/-1; padding:40px; text-align:center; color:#94a3b8;">This folder is empty.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
gridEl.innerHTML = filtered.map(f => {
|
||||
let icon = 'FILE';
|
||||
if (f.type === 'spreadsheet') icon = 'XLSX';
|
||||
if (f.type === 'pdf') icon = 'PDF';
|
||||
if (f.type === 'archive') icon = 'ZIP';
|
||||
if (f.type === 'executable') icon = 'EXE';
|
||||
|
||||
return `
|
||||
<div class="file-item" data-file-id="${f.id}" title="${f.name} (${f.size})">
|
||||
<div style="font-weight:700; font-size:18px; padding:8px 0; color:#2563eb;">[${icon}]</div>
|
||||
<div class="file-name">${f.name}</div>
|
||||
<div class="file-size">${f.size}</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
gridEl.querySelectorAll('.file-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
gridEl.querySelectorAll('.file-item').forEach(i => i.classList.remove('selected'));
|
||||
item.classList.add('selected');
|
||||
});
|
||||
|
||||
item.addEventListener('dblclick', () => {
|
||||
const fileId = item.dataset.fileId;
|
||||
const fileObj = this.files.find(f => f.id === fileId);
|
||||
if (fileObj) {
|
||||
this.eventBus.emit('FILE_OPENED', {
|
||||
target: fileObj.name,
|
||||
details: { fileId: fileObj.id, folder: fileObj.folder, type: fileObj.type }
|
||||
});
|
||||
if (this.onOpenFile) {
|
||||
this.onOpenFile(fileObj);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
/**
|
||||
* CyberSim OS - Inlook Email Application
|
||||
*/
|
||||
|
||||
export class InlookApp {
|
||||
constructor({ windowManager, eventBus, notifications, scenario, onNavigateUrl, onOpenDoc, onFileDownloaded }) {
|
||||
this.wm = windowManager;
|
||||
this.eventBus = eventBus;
|
||||
this.notifications = notifications;
|
||||
this.scenario = scenario;
|
||||
this.onNavigateUrl = onNavigateUrl;
|
||||
this.onOpenDoc = onOpenDoc;
|
||||
this.onFileDownloaded = onFileDownloaded;
|
||||
this.currentFolder = 'inbox';
|
||||
this.activeEmailId = null;
|
||||
this.emails = JSON.parse(JSON.stringify(scenario.emails));
|
||||
}
|
||||
|
||||
getIconSvg() {
|
||||
return `<svg viewBox="0 0 24 24" fill="none" stroke="#2563eb" stroke-width="2"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>`;
|
||||
}
|
||||
|
||||
launch() {
|
||||
const win = this.wm.createWindow({
|
||||
id: 'inlook',
|
||||
title: 'Inlook Mail - NexaCore Workplace',
|
||||
iconSvg: this.getIconSvg(),
|
||||
width: 860,
|
||||
height: 560,
|
||||
bodyContent: this.renderShell()
|
||||
});
|
||||
|
||||
this.bindEvents(win.bodyElement);
|
||||
this.selectFirstEmail();
|
||||
this.eventBus.emit('APP_OPENED', { target: 'inlook' });
|
||||
}
|
||||
|
||||
renderShell() {
|
||||
return `
|
||||
<div class="inlook-container">
|
||||
<div class="inlook-sidebar">
|
||||
<button class="cs-btn cs-btn-primary cs-btn-sm" style="margin-bottom:8px;" id="inlook-compose-btn">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>
|
||||
New Message
|
||||
</button>
|
||||
<div class="inlook-folder-item active" data-folder="inbox">
|
||||
<span>📥 Inbox</span>
|
||||
<span class="cs-badge cs-badge-info" id="inlook-unread-count">0</span>
|
||||
</div>
|
||||
<div class="inlook-folder-item" data-folder="sent">
|
||||
<span>📤 Sent</span>
|
||||
</div>
|
||||
<div class="inlook-folder-item" data-folder="trash">
|
||||
<span>🗑️ Trash</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inlook-list-pane" id="inlook-email-list"></div>
|
||||
<div class="inlook-reading-pane" id="inlook-reading-pane">
|
||||
<div style="padding:40px; text-align:center; color:#94a3b8; margin:auto;">
|
||||
Select an email message to view its contents.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
bindEvents(container) {
|
||||
container.querySelectorAll('.inlook-folder-item').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
container.querySelectorAll('.inlook-folder-item').forEach(f => f.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
this.currentFolder = el.dataset.folder;
|
||||
this.renderEmailList();
|
||||
});
|
||||
});
|
||||
|
||||
const composeBtn = container.querySelector('#inlook-compose-btn');
|
||||
if (composeBtn) {
|
||||
composeBtn.addEventListener('click', () => {
|
||||
this.notifications.show({
|
||||
title: 'Inlook Mail',
|
||||
body: 'Corporate policy restricts unassigned outgoing mail during initial shift orientation.',
|
||||
type: 'info'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this.renderEmailList();
|
||||
}
|
||||
|
||||
renderEmailList() {
|
||||
const listEl = document.getElementById('inlook-email-list');
|
||||
if (!listEl) return;
|
||||
|
||||
const filtered = this.emails.filter(e => e.folder === this.currentFolder);
|
||||
const unread = this.emails.filter(e => e.folder === 'inbox' && e.unread).length;
|
||||
const badge = document.getElementById('inlook-unread-count');
|
||||
if (badge) badge.innerText = unread;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
listEl.innerHTML = `<div style="padding:20px; text-align:center; color:#94a3b8; font-size:12px;">No messages in ${this.currentFolder}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = filtered.map(email => `
|
||||
<div class="inlook-list-item ${email.unread ? 'unread' : ''} ${email.id === this.activeEmailId ? 'active' : ''}" data-email-id="${email.id}">
|
||||
<div class="inlook-item-header">
|
||||
<span class="inlook-item-sender">${email.sender.split('(')[0]}</span>
|
||||
<span class="inlook-item-date">${email.date}</span>
|
||||
</div>
|
||||
<div class="inlook-item-subject">${email.subject}</div>
|
||||
<div class="inlook-item-snippet">${email.body.replace(/<[^>]*>?/gm, '').substring(0, 48)}...</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
listEl.querySelectorAll('.inlook-list-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const id = item.dataset.emailId;
|
||||
this.openEmail(id);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
selectFirstEmail() {
|
||||
const inboxEmails = this.emails.filter(e => e.folder === 'inbox');
|
||||
if (inboxEmails.length > 0) {
|
||||
this.openEmail(inboxEmails[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
openEmail(id) {
|
||||
const email = this.emails.find(e => e.id === id);
|
||||
if (!email) return;
|
||||
|
||||
this.activeEmailId = id;
|
||||
email.unread = false;
|
||||
this.renderEmailList();
|
||||
|
||||
this.eventBus.emit('EMAIL_OPENED', {
|
||||
target: id,
|
||||
details: { subject: email.subject, rfcSender: email.rfcSender, isThreat: !!email.isThreat }
|
||||
});
|
||||
|
||||
const readingPane = document.getElementById('inlook-reading-pane');
|
||||
if (!readingPane) return;
|
||||
|
||||
readingPane.innerHTML = `
|
||||
<div class="inlook-toolbar">
|
||||
<button class="cs-btn cs-btn-sm" id="btn-inlook-reply">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 17 4 12 9 7"/><path d="M20 18v-2a4 4 0 0 0-4-4H4"/></svg> Reply
|
||||
</button>
|
||||
<button class="cs-btn cs-btn-danger cs-btn-sm" id="btn-inlook-report">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M3.6 9h16.8L12 21 3.6 9z"/></svg> Report Suspicious
|
||||
</button>
|
||||
<button class="cs-btn cs-btn-sm" id="btn-inlook-delete">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/></svg> Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="inlook-msg-header">
|
||||
<div class="inlook-msg-subject">${email.subject}</div>
|
||||
<div class="inlook-sender-row">
|
||||
<div class="inlook-sender-box">
|
||||
<div class="inlook-sender-avatar">${email.sender.charAt(0)}</div>
|
||||
<div class="inlook-sender-details">
|
||||
<div class="sender-name">${email.sender}</div>
|
||||
<div class="sender-email">${email.rfcSender}</div>
|
||||
</div>
|
||||
<button class="inlook-inspect-btn" id="btn-inspect-header">Inspect Header</button>
|
||||
</div>
|
||||
<div style="font-size:11px; color:#94a3b8;">${email.date}</div>
|
||||
</div>
|
||||
|
||||
${email.attachments && email.attachments.length > 0 ? `
|
||||
<div class="inlook-attachments">
|
||||
<span style="font-size:11px; font-weight:600; color:#64748b;">Attachments (${email.attachments.length}):</span>
|
||||
${email.attachments.map(att => `
|
||||
<div class="inlook-attachment-chip" data-att-name="${att.name}" data-att-size="${att.size}">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
||||
<span>${att.name}</span>
|
||||
<span style="color:#94a3b8; font-size:10px;">(${att.size})</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="inlook-msg-body">
|
||||
${email.body.replace(/\n/g, '<br>')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.bindEmailActions(email);
|
||||
}
|
||||
|
||||
bindEmailActions(email) {
|
||||
const inspectBtn = document.getElementById('btn-inspect-header');
|
||||
if (inspectBtn) {
|
||||
inspectBtn.addEventListener('click', () => {
|
||||
this.eventBus.emit('EMAIL_INSPECTED_SENDER', {
|
||||
target: email.id,
|
||||
details: { displaySender: email.sender, rfcSender: email.rfcSender }
|
||||
});
|
||||
|
||||
const isDomainMismatch = email.rfcSender.includes('nexac0re-portal.com') || email.rfcSender.includes('apex-global-supplies.net');
|
||||
|
||||
alert(`--- Inlook RFC Header Inspector ---\n\nDisplay Name: ${email.sender}\nEnvelope RFC From: <${email.rfcSender}>\nAuthentication-Results: spf=pass (domain: ${email.rfcSender.split('@')[1]})\nReturn-Path: <bounce@${email.rfcSender.split('@')[1]}>\n\n${isDomainMismatch ? '⚠️ Notice: Envelope domain differs from standard corporate (@nexacore.internal).' : '✓ Envelope domain matches internal enterprise domain.'}`);
|
||||
});
|
||||
}
|
||||
|
||||
const links = document.querySelectorAll('.inlook-msg-link');
|
||||
links.forEach(link => {
|
||||
const actualUrl = link.dataset.url;
|
||||
const displayText = link.dataset.display || link.innerText;
|
||||
|
||||
link.title = `Simulated Target: ${actualUrl}`;
|
||||
|
||||
link.addEventListener('mouseenter', () => {
|
||||
this.eventBus.emit('EMAIL_LINK_HOVERED', {
|
||||
target: email.id,
|
||||
details: { displayText, actualUrl }
|
||||
});
|
||||
});
|
||||
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
this.eventBus.emit('EMAIL_LINK_CLICKED', {
|
||||
target: email.id,
|
||||
details: { displayText, actualUrl, isPhishing: actualUrl.includes('nexac0re-portal.com') }
|
||||
});
|
||||
|
||||
if (this.onNavigateUrl) {
|
||||
this.onNavigateUrl(actualUrl);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const attChips = document.querySelectorAll('.inlook-attachment-chip');
|
||||
attChips.forEach(chip => {
|
||||
chip.addEventListener('click', () => {
|
||||
const attName = chip.dataset.attName;
|
||||
const attSize = chip.dataset.attSize;
|
||||
|
||||
this.eventBus.emit('EMAIL_ATTACHMENT_OPENED', {
|
||||
target: email.id,
|
||||
details: { attachmentName: attName, attachmentSize: attSize }
|
||||
});
|
||||
|
||||
if (this.onFileDownloaded) {
|
||||
this.onFileDownloaded({
|
||||
id: `down_${Date.now()}`,
|
||||
name: attName,
|
||||
type: 'archive',
|
||||
folder: 'Downloads',
|
||||
size: attSize,
|
||||
date: new Date().toISOString().split('T')[0]
|
||||
});
|
||||
}
|
||||
|
||||
this.notifications.show({
|
||||
title: 'Download Complete',
|
||||
body: `File "${attName}" saved to Downloads folder.`,
|
||||
type: 'warning'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const reportBtn = document.getElementById('btn-inlook-report');
|
||||
if (reportBtn) {
|
||||
reportBtn.addEventListener('click', () => {
|
||||
this.openReportDialog(email);
|
||||
});
|
||||
}
|
||||
|
||||
const replyBtn = document.getElementById('btn-inlook-reply');
|
||||
if (replyBtn) {
|
||||
replyBtn.addEventListener('click', () => {
|
||||
this.openReplyDialog(email);
|
||||
});
|
||||
}
|
||||
|
||||
const deleteBtn = document.getElementById('btn-inlook-delete');
|
||||
if (deleteBtn) {
|
||||
deleteBtn.addEventListener('click', () => {
|
||||
email.folder = 'trash';
|
||||
this.renderEmailList();
|
||||
this.eventBus.emit('EMAIL_DELETED', {
|
||||
target: email.id,
|
||||
details: { subject: email.subject, wasThreat: !!email.isThreat }
|
||||
});
|
||||
this.selectFirstEmail();
|
||||
this.notifications.show({
|
||||
title: 'Inlook Mail',
|
||||
body: 'Message moved to Trash.',
|
||||
type: 'info'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openReportDialog(email) {
|
||||
const modalOverlay = document.createElement('div');
|
||||
modalOverlay.className = 'cs-modal-overlay';
|
||||
modalOverlay.innerHTML = `
|
||||
<div class="cs-modal">
|
||||
<div class="cs-modal-header">
|
||||
<div class="cs-modal-title">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 9v4"/><path d="M12 17h.01"/><path d="M3.6 9h16.8L12 21 3.6 9z"/></svg>
|
||||
Report Suspicious Message to SOC
|
||||
</div>
|
||||
<button class="cs-btn-win close" id="btn-close-modal">×</button>
|
||||
</div>
|
||||
<div class="cs-modal-body">
|
||||
<p style="margin-bottom:12px;">You are about to report the following email to the NexaCore Security Operations Center:</p>
|
||||
<div style="background:#f1f5f9; padding:10px; border-radius:4px; font-size:12px; margin-bottom:14px;">
|
||||
<strong>Subject:</strong> ${email.subject}<br>
|
||||
<strong>Sender:</strong> ${email.rfcSender}
|
||||
</div>
|
||||
<div class="cs-form-group">
|
||||
<label class="cs-form-label">Select Primary Threat Reason:</label>
|
||||
<select class="cs-select" id="report-reason">
|
||||
<option value="spoofed_sender">Spoofed or lookalike sender domain</option>
|
||||
<option value="deceptive_link">Deceptive / mismatched link destination</option>
|
||||
<option value="suspicious_attachment">Unsolicited or executable attachment</option>
|
||||
<option value="credential_harvest">Credential theft / artificial urgency</option>
|
||||
<option value="general_spam">General unsolicited spam</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="cs-form-group">
|
||||
<label class="cs-form-label">Investigative Notes (Optional):</label>
|
||||
<textarea class="cs-textarea" id="report-notes" rows="2" placeholder="e.g. Lookalike domain nexac0re-portal.com detected"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cs-modal-footer">
|
||||
<button class="cs-btn" id="btn-cancel-report">Cancel</button>
|
||||
<button class="cs-btn cs-btn-danger" id="btn-submit-report">Submit Incident Report</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modalOverlay);
|
||||
const close = () => modalOverlay.remove();
|
||||
modalOverlay.querySelector('#btn-close-modal').addEventListener('click', close);
|
||||
modalOverlay.querySelector('#btn-cancel-report').addEventListener('click', close);
|
||||
|
||||
modalOverlay.querySelector('#btn-submit-report').addEventListener('click', () => {
|
||||
const reason = modalOverlay.querySelector('#report-reason').value;
|
||||
const notes = modalOverlay.querySelector('#report-notes').value;
|
||||
|
||||
this.eventBus.emit('EMAIL_REPORTED', {
|
||||
target: email.id,
|
||||
details: { reason, notes, isThreat: !!email.isThreat, threatId: email.threatId || null }
|
||||
});
|
||||
|
||||
email.folder = 'trash';
|
||||
this.renderEmailList();
|
||||
this.selectFirstEmail();
|
||||
close();
|
||||
|
||||
this.notifications.show({
|
||||
title: 'Security Center Report Acknowledged',
|
||||
body: `Incident report for "${email.subject.substring(0, 30)}..." received by SOC.`,
|
||||
type: 'success'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
openReplyDialog(email) {
|
||||
const modalOverlay = document.createElement('div');
|
||||
modalOverlay.className = 'cs-modal-overlay';
|
||||
modalOverlay.innerHTML = `
|
||||
<div class="cs-modal">
|
||||
<div class="cs-modal-header">
|
||||
<div class="cs-modal-title">Reply to: ${email.sender}</div>
|
||||
<button class="cs-btn-win close" id="btn-close-reply">×</button>
|
||||
</div>
|
||||
<div class="cs-modal-body">
|
||||
<div class="cs-form-group">
|
||||
<label class="cs-form-label">To:</label>
|
||||
<input type="text" class="cs-input" value="${email.rfcSender}" readonly>
|
||||
</div>
|
||||
<div class="cs-form-group">
|
||||
<label class="cs-form-label">Subject:</label>
|
||||
<input type="text" class="cs-input" value="Re: ${email.subject}" readonly>
|
||||
</div>
|
||||
<div class="cs-form-group">
|
||||
<label class="cs-form-label">Response Message:</label>
|
||||
<textarea class="cs-textarea" id="reply-text" rows="4" placeholder="Type your workplace response here..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cs-modal-footer">
|
||||
<button class="cs-btn" id="btn-cancel-reply">Cancel</button>
|
||||
<button class="cs-btn cs-btn-primary" id="btn-send-reply">Send Reply</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modalOverlay);
|
||||
const close = () => modalOverlay.remove();
|
||||
modalOverlay.querySelector('#btn-close-reply').addEventListener('click', close);
|
||||
modalOverlay.querySelector('#btn-cancel-reply').addEventListener('click', close);
|
||||
|
||||
modalOverlay.querySelector('#btn-send-reply').addEventListener('click', () => {
|
||||
const text = modalOverlay.querySelector('#reply-text').value;
|
||||
this.eventBus.emit('EMAIL_REPLIED', {
|
||||
target: email.id,
|
||||
details: { responseText: text, recipient: email.rfcSender }
|
||||
});
|
||||
close();
|
||||
this.notifications.show({
|
||||
title: 'Inlook Mail',
|
||||
body: `Reply sent to ${email.sender.split('(')[0]}.`,
|
||||
type: 'success'
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
/**
|
||||
* CyberSim OS - Navigator Simulated Web Browser
|
||||
*/
|
||||
|
||||
export class NavigatorApp {
|
||||
constructor({ windowManager, eventBus, notifications, scenario }) {
|
||||
this.wm = windowManager;
|
||||
this.eventBus = eventBus;
|
||||
this.notifications = notifications;
|
||||
this.scenario = scenario;
|
||||
this.currentUrl = 'http://intranet.nexacore.internal';
|
||||
this.history = [this.currentUrl];
|
||||
this.historyIndex = 0;
|
||||
}
|
||||
|
||||
getIconSvg() {
|
||||
return `<svg viewBox="0 0 24 24" fill="none" stroke="#0284c7" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>`;
|
||||
}
|
||||
|
||||
launch(initialUrl = null) {
|
||||
const url = initialUrl || this.currentUrl;
|
||||
const win = this.wm.createWindow({
|
||||
id: 'navigator',
|
||||
title: 'Navigator Web Browser',
|
||||
iconSvg: this.getIconSvg(),
|
||||
width: 880,
|
||||
height: 580,
|
||||
bodyContent: this.renderShell()
|
||||
});
|
||||
|
||||
this.bindEvents(win.bodyElement);
|
||||
this.navigateTo(url);
|
||||
this.eventBus.emit('APP_OPENED', { target: 'navigator' });
|
||||
}
|
||||
|
||||
renderShell() {
|
||||
return `
|
||||
<div class="nav-container">
|
||||
<div class="nav-toolbar">
|
||||
<div class="nav-buttons">
|
||||
<button class="nav-btn-icon" id="nav-btn-back" title="Back">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>
|
||||
</button>
|
||||
<button class="nav-btn-icon" id="nav-btn-forward" title="Forward">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>
|
||||
</button>
|
||||
<button class="nav-btn-icon" id="nav-btn-reload" title="Reload">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="nav-address-bar secure" id="nav-addr-bar">
|
||||
<div class="nav-lock-icon" id="nav-lock-icon">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
|
||||
</div>
|
||||
<input type="text" class="nav-url-input" id="nav-url-input" value="${this.currentUrl}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-viewport" id="nav-viewport"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
bindEvents(container) {
|
||||
const urlInput = container.querySelector('#nav-url-input');
|
||||
const backBtn = container.querySelector('#nav-btn-back');
|
||||
const forwardBtn = container.querySelector('#nav-btn-forward');
|
||||
const reloadBtn = container.querySelector('#nav-btn-reload');
|
||||
|
||||
urlInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
this.navigateTo(urlInput.value.trim());
|
||||
}
|
||||
});
|
||||
|
||||
backBtn.addEventListener('click', () => {
|
||||
if (this.historyIndex > 0) {
|
||||
this.historyIndex--;
|
||||
this.navigateTo(this.history[this.historyIndex], false);
|
||||
}
|
||||
});
|
||||
|
||||
forwardBtn.addEventListener('click', () => {
|
||||
if (this.historyIndex < this.history.length - 1) {
|
||||
this.historyIndex++;
|
||||
this.navigateTo(this.history[this.historyIndex], false);
|
||||
}
|
||||
});
|
||||
|
||||
reloadBtn.addEventListener('click', () => {
|
||||
this.navigateTo(this.currentUrl, false);
|
||||
});
|
||||
}
|
||||
|
||||
navigateTo(url, recordHistory = true) {
|
||||
this.currentUrl = url;
|
||||
if (recordHistory) {
|
||||
this.history = this.history.slice(0, this.historyIndex + 1);
|
||||
this.history.push(url);
|
||||
this.historyIndex = this.history.length - 1;
|
||||
}
|
||||
|
||||
const urlInput = document.getElementById('nav-url-input');
|
||||
const addrBar = document.getElementById('nav-addr-bar');
|
||||
const lockIcon = document.getElementById('nav-lock-icon');
|
||||
const viewport = document.getElementById('nav-viewport');
|
||||
|
||||
if (urlInput) urlInput.value = url;
|
||||
|
||||
const isSecure = url.startsWith('https://') || url.includes('.nexacore.internal');
|
||||
const isPhish = url.includes('nexac0re-portal.com');
|
||||
|
||||
if (addrBar) {
|
||||
addrBar.className = `nav-address-bar ${isSecure ? 'secure' : 'insecure'}`;
|
||||
}
|
||||
if (lockIcon) {
|
||||
lockIcon.style.color = isSecure ? '#10b981' : '#f59e0b';
|
||||
}
|
||||
|
||||
this.eventBus.emit('NAV_VISITED', {
|
||||
target: url,
|
||||
details: { isSecure, isPhishing: isPhish }
|
||||
});
|
||||
|
||||
const page = this.scenario.pages.find(p => p.url === url) || this.scenario.pages.find(p => url.startsWith(p.url));
|
||||
|
||||
if (viewport) {
|
||||
if (page) {
|
||||
viewport.innerHTML = page.content;
|
||||
this.bindPageLinks(viewport);
|
||||
} else {
|
||||
viewport.innerHTML = `
|
||||
<div style="padding:60px 20px; text-align:center;">
|
||||
<h2 style="color:#64748b; font-size:18px; margin-bottom:8px;">404 Page Not Found</h2>
|
||||
<p style="color:#94a3b8; font-size:13px;">The simulated URL <code>${url}</code> was not found on this simulation network.</p>
|
||||
<button class="cs-btn cs-btn-primary" style="margin-top:16px;" onclick="window.CyberSimOS.navigator.navigateTo('http://intranet.nexacore.internal')">Go to Intranet Home</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bindPageLinks(viewport) {
|
||||
viewport.querySelectorAll('a').forEach(a => {
|
||||
a.addEventListener('click', (e) => {
|
||||
const href = a.getAttribute('href');
|
||||
if (href && (href.startsWith('http://') || href.startsWith('https://'))) {
|
||||
e.preventDefault();
|
||||
this.navigateTo(href);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handlePhishFormSubmit(form) {
|
||||
const user = form.querySelector('#phish_user').value;
|
||||
const pass = form.querySelector('#phish_pass').value;
|
||||
|
||||
this.eventBus.emit('NAV_FORM_SUBMITTED', {
|
||||
target: 'phish_login_form',
|
||||
details: {
|
||||
url: this.currentUrl,
|
||||
usernameEntered: user,
|
||||
submittedPassword: !!pass,
|
||||
isCompromised: true
|
||||
}
|
||||
});
|
||||
|
||||
this.notifications.show({
|
||||
title: 'Identity Portal',
|
||||
body: 'Credentials accepted. Identity synchronization in progress...',
|
||||
type: 'info'
|
||||
});
|
||||
|
||||
const viewport = document.getElementById('nav-viewport');
|
||||
if (viewport) {
|
||||
viewport.innerHTML = `
|
||||
<div style="padding:60px 20px; text-align:center; max-width:480px; margin:auto;">
|
||||
<div style="font-size:36px; margin-bottom:12px;">✔</div>
|
||||
<h2 style="color:#0f172a; font-size:18px; margin-bottom:8px;">Account Verification Complete</h2>
|
||||
<p style="color:#64748b; font-size:13px; line-height:1.5;">Your credentials have been verified with our external single sign-on synchronization gateway. You may return to your workplace desktop.</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* CyberSim OS - Security Center Application
|
||||
*/
|
||||
|
||||
export class SecurityCenterApp {
|
||||
constructor({ windowManager, eventBus }) {
|
||||
this.wm = windowManager;
|
||||
this.eventBus = eventBus;
|
||||
this.alerts = [
|
||||
{
|
||||
id: 'alert_initial_status',
|
||||
title: 'Endpoint Threat Protection Active',
|
||||
severity: 'info',
|
||||
source: 'NexaCore Endpoint Agent',
|
||||
message: 'Endpoint sensor status: Healthy. Definitions version 2026.08.24-1.',
|
||||
timestamp: '08:30 AM'
|
||||
}
|
||||
];
|
||||
this.reports = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.eventBus.on('EMAIL_REPORTED', (entry) => {
|
||||
this.reports.push({
|
||||
id: `rep_${Date.now()}`,
|
||||
target: entry.target,
|
||||
reason: entry.details.reason,
|
||||
notes: entry.details.notes,
|
||||
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
|
||||
status: 'Acknowledged by SOC'
|
||||
});
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
getIconSvg() {
|
||||
return `<svg viewBox="0 0 24 24" fill="none" stroke="#dc2626" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>`;
|
||||
}
|
||||
|
||||
addAlert(alertObj) {
|
||||
this.alerts.unshift(alertObj);
|
||||
this.render();
|
||||
}
|
||||
|
||||
launch() {
|
||||
const win = this.wm.createWindow({
|
||||
id: 'security_center',
|
||||
title: 'NexaCore Security Center',
|
||||
iconSvg: this.getIconSvg(),
|
||||
width: 760,
|
||||
height: 520,
|
||||
bodyContent: this.renderShell()
|
||||
});
|
||||
|
||||
this.render();
|
||||
this.eventBus.emit('APP_OPENED', { target: 'security_center' });
|
||||
}
|
||||
|
||||
renderShell() {
|
||||
return `
|
||||
<div class="sec-center-container">
|
||||
<div class="sec-sidebar">
|
||||
<div style="font-size:13px; font-weight:700; color:#ffffff; margin-bottom:12px; display:flex; align-items:center; gap:8px;">
|
||||
${this.getIconSvg()} Security Center
|
||||
</div>
|
||||
<div class="sec-nav-btn active" data-sec-tab="overview">Dashboard</div>
|
||||
<div class="sec-nav-btn" data-sec-tab="alerts">Alerts & Logs</div>
|
||||
<div class="sec-nav-btn" data-sec-tab="reports">Reported Incidents</div>
|
||||
</div>
|
||||
<div class="sec-main" id="sec-main-content"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
render() {
|
||||
const main = document.getElementById('sec-main-content');
|
||||
if (!main) return;
|
||||
|
||||
const hasHighAlert = this.alerts.some(a => a.severity === 'high');
|
||||
|
||||
main.innerHTML = `
|
||||
<div class="sec-header">
|
||||
<div class="sec-title">Workstation Security Status</div>
|
||||
<div style="font-size:12px; color:#64748b;">NexaCore Enterprise Zero-Trust Endpoint Protection</div>
|
||||
</div>
|
||||
|
||||
<div class="sec-status-banner ${hasHighAlert ? 'alert-state' : ''}">
|
||||
<div style="font-size:28px;">${hasHighAlert ? '⚠️' : '🛡️'}</div>
|
||||
<div>
|
||||
<div style="font-weight:700; font-size:14px;">${hasHighAlert ? 'Security Attention Required' : 'Workstation Protected & Monitored'}</div>
|
||||
<div style="font-size:12px;">${hasHighAlert ? 'One or more high severity security alerts require attention.' : 'All security telemetry feeds are operational. Zero active threats detected.'}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:14px; font-weight:600; margin-bottom:10px;">Recent Security Alerts & Notifications</h3>
|
||||
<div class="sec-alert-list">
|
||||
${this.alerts.map(a => `
|
||||
<div class="sec-alert-item ${a.severity}">
|
||||
<div style="flex:1;">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:3px;">
|
||||
<span style="font-weight:600; font-size:12px; color:#0f172a;">${a.title}</span>
|
||||
<span style="font-size:11px; color:#94a3b8;">${a.timestamp}</span>
|
||||
</div>
|
||||
<div style="font-size:11px; color:#475569; line-height:1.4;">${a.message}</div>
|
||||
<div style="font-size:10px; color:#94a3b8; margin-top:4px;">Source: ${a.source}</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:14px; font-weight:600; margin:20px 0 10px 0;">User Incident Reports (${this.reports.length})</h3>
|
||||
${this.reports.length === 0 ? `
|
||||
<div style="padding:14px; background:#ffffff; border:1px dashed #cbd5e1; border-radius:6px; font-size:12px; color:#94a3b8; text-align:center;">
|
||||
No suspicious messages or incidents reported yet during this shift.
|
||||
</div>
|
||||
` : `
|
||||
<div style="display:flex; flex-direction:column; gap:8px;">
|
||||
${this.reports.map(r => `
|
||||
<div style="background:#ffffff; border:1px solid #cbd5e1; border-radius:6px; padding:10px 14px; display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:12px;">Report: ${r.reason.replace(/_/g, ' ')}</div>
|
||||
<div style="font-size:11px; color:#64748b;">Target: ${r.target} | Time: ${r.time}</div>
|
||||
</div>
|
||||
<span class="cs-badge cs-badge-success">${r.status}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* CyberSim OS - Cryptographic Certificate Generator & *.cybercert Exporter
|
||||
*/
|
||||
|
||||
export class CertificateGenerator {
|
||||
constructor(scenario, scoreResult, scenarioFingerprint) {
|
||||
this.scenario = scenario;
|
||||
this.scoreResult = scoreResult;
|
||||
this.scenarioFingerprint = scenarioFingerprint;
|
||||
}
|
||||
|
||||
generateUUID() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
||||
const r = Math.random() * 16 | 0, v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
async createCertificateData(learnerName = 'Jordan Taylor') {
|
||||
const certId = this.generateUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const engineVersion = '1.0.0-phase1';
|
||||
|
||||
const payload = {
|
||||
schema_version: 1,
|
||||
certificate_id: certId,
|
||||
product: 'CyberSim OS',
|
||||
engine_version: engineVersion,
|
||||
scenario_id: this.scenario.scenarioId,
|
||||
scenario_title: this.scenario.title,
|
||||
scenario_version: this.scenario.version,
|
||||
scenario_fingerprint: this.scenarioFingerprint,
|
||||
learner: {
|
||||
name: learnerName,
|
||||
assigned_role: this.scenario.learner.role,
|
||||
organization: this.scenario.company.name
|
||||
},
|
||||
evaluation: {
|
||||
score: this.scoreResult.totalScore,
|
||||
max_score: 100,
|
||||
passed: this.scoreResult.isPassed,
|
||||
passing_threshold: this.scoreResult.passingThreshold,
|
||||
category_scores: this.scoreResult.categories
|
||||
},
|
||||
issued_at: timestamp,
|
||||
trust_model: 'self_issued_cryptographic_record'
|
||||
};
|
||||
|
||||
// Calculate integrity checksum over canonical record
|
||||
const canonicalStr = `${certId}:${learnerName}:${this.scenario.scenarioId}:${this.scenarioFingerprint}:${this.scoreResult.totalScore}:${timestamp}`;
|
||||
const hashBuffer = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonicalStr));
|
||||
const integrityHash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
payload.integrity_hash = integrityHash;
|
||||
return payload;
|
||||
}
|
||||
|
||||
exportFile(certData) {
|
||||
const jsonStr = JSON.stringify(certData, null, 2);
|
||||
const blob = new Blob([jsonStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `CyberSim_Certificate_${certData.certificate_id.substring(0, 8)}.cybercert`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async showCertificateModal(learnerName = 'Jordan Taylor') {
|
||||
const certData = await this.createCertificateData(learnerName);
|
||||
const modalOverlay = document.createElement('div');
|
||||
modalOverlay.className = 'cs-modal-overlay';
|
||||
modalOverlay.style.zIndex = '9500';
|
||||
|
||||
modalOverlay.innerHTML = `
|
||||
<div class="cs-modal" style="max-width:640px;">
|
||||
<div class="cs-modal-header">
|
||||
<div class="cs-modal-title">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/></svg>
|
||||
CyberSim OS - Official Certificate of Competency
|
||||
</div>
|
||||
<button class="cs-btn-win close" id="btn-close-cert-modal">×</button>
|
||||
</div>
|
||||
|
||||
<div class="cs-modal-body" style="background:#f8fafc; padding:20px;">
|
||||
<div class="cert-printable" id="cert-printable-area">
|
||||
<div class="cert-org">NexaCore Technologies • CyberSim Environment</div>
|
||||
<div class="cert-title">Certificate of Competency</div>
|
||||
<div class="cert-subtitle">End-User Cybersecurity Simulation & Behavioral Verification</div>
|
||||
|
||||
<div style="font-size:12px; color:#64748b; margin-top:14px;">This certifies that</div>
|
||||
<div class="cert-recipient">${certData.learner.name}</div>
|
||||
|
||||
<div class="cert-text">
|
||||
has successfully completed the <strong>${certData.scenario_title}</strong> simulation, demonstrating sound investigative judgment, threat detection, safe credential handling, and policy adherence.
|
||||
</div>
|
||||
|
||||
<div style="font-size:14px; font-weight:700; color:#166534; background:#dcfce7; display:inline-block; padding:4px 16px; border-radius:12px; margin-bottom:14px;">
|
||||
Final Score: ${certData.evaluation.score} / 100 (PASSED)
|
||||
</div>
|
||||
|
||||
<div class="cert-meta-grid">
|
||||
<div>
|
||||
<strong>Certificate ID:</strong> ${certData.certificate_id}<br>
|
||||
<strong>Date:</strong> ${new Date(certData.issued_at).toLocaleDateString()}<br>
|
||||
<strong>Engine Version:</strong> ${certData.engine_version}
|
||||
</div>
|
||||
<div style="word-break:break-all;">
|
||||
<strong>Scenario Hash:</strong><br>${certData.scenario_fingerprint.substring(0, 32)}...<br>
|
||||
<strong>Integrity Hash:</strong><br>${certData.integrity_hash.substring(0, 32)}...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cs-modal-footer">
|
||||
<button class="cs-btn" id="btn-print-cert">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg> Print
|
||||
</button>
|
||||
<button class="cs-btn cs-btn-primary" id="btn-download-cybercert">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> Export *.cybercert File
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modalOverlay);
|
||||
|
||||
const close = () => modalOverlay.remove();
|
||||
modalOverlay.querySelector('#btn-close-cert-modal').addEventListener('click', close);
|
||||
|
||||
modalOverlay.querySelector('#btn-download-cybercert').addEventListener('click', () => {
|
||||
this.exportFile(certData);
|
||||
});
|
||||
|
||||
modalOverlay.querySelector('#btn-print-cert').addEventListener('click', () => {
|
||||
window.print();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* CyberSim OS - Offline Certificate Verifier
|
||||
* Validates *.cybercert structured JSON cryptographic records offline.
|
||||
*/
|
||||
|
||||
export class CertificateVerifier {
|
||||
static async verify(certData) {
|
||||
try {
|
||||
if (!certData || typeof certData !== 'object') {
|
||||
return { valid: false, error: 'Invalid certificate payload: not a JSON object.' };
|
||||
}
|
||||
|
||||
// Check required schema fields
|
||||
const required = ['schema_version', 'certificate_id', 'product', 'scenario_id', 'scenario_fingerprint', 'learner', 'evaluation', 'issued_at', 'integrity_hash'];
|
||||
for (const field of required) {
|
||||
if (!(field in certData)) {
|
||||
return { valid: false, error: `Missing required certificate field: ${field}` };
|
||||
}
|
||||
}
|
||||
|
||||
// Verify integrity hash
|
||||
const certId = certData.certificate_id;
|
||||
const learnerName = certData.learner.name;
|
||||
const scenarioId = certData.scenario_id;
|
||||
const fingerprint = certData.scenario_fingerprint;
|
||||
const score = certData.evaluation.score;
|
||||
const timestamp = certData.issued_at;
|
||||
|
||||
const canonicalStr = `${certId}:${learnerName}:${scenarioId}:${fingerprint}:${score}:${timestamp}`;
|
||||
const hashBuffer = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonicalStr));
|
||||
const calculatedHash = Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
|
||||
const hashValid = (calculatedHash.toLowerCase() === certData.integrity_hash.toLowerCase());
|
||||
if (!hashValid) {
|
||||
return {
|
||||
valid: false,
|
||||
error: 'Cryptographic Checksum Mismatch: This certificate record has been tampered with or modified.',
|
||||
certData
|
||||
};
|
||||
}
|
||||
|
||||
const passed = certData.evaluation.passed && certData.evaluation.score >= (certData.evaluation.passing_threshold || 80);
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
passed,
|
||||
certData,
|
||||
message: 'Certificate cryptographic integrity verified successfully.'
|
||||
};
|
||||
} catch (err) {
|
||||
return { valid: false, error: `Verification failed: ${err.message}` };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* CyberSim OS - Desktop Shell & Taskbar Interface
|
||||
*/
|
||||
|
||||
export class DesktopShell {
|
||||
constructor({ desktopElement, startMenuElement, startBtnElement, clockElement, eventBus, onFinishScenario }) {
|
||||
this.desktop = desktopElement;
|
||||
this.startMenu = startMenuElement;
|
||||
this.startBtn = startBtnElement;
|
||||
this.clock = clockElement;
|
||||
this.eventBus = eventBus;
|
||||
this.onFinishScenario = onFinishScenario;
|
||||
this.apps = [];
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Toggle Start Menu
|
||||
this.startBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleStartMenu();
|
||||
});
|
||||
|
||||
// Close Start Menu on Outside Click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (this.startMenu.classList.contains('open') && !this.startMenu.contains(e.target) && !this.startBtn.contains(e.target)) {
|
||||
this.closeStartMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Start Clock
|
||||
this.updateClock();
|
||||
setInterval(() => this.updateClock(), 1000);
|
||||
|
||||
// End Simulation Button
|
||||
const finishBtn = document.getElementById('start-btn-finish');
|
||||
if (finishBtn) {
|
||||
finishBtn.addEventListener('click', () => {
|
||||
this.closeStartMenu();
|
||||
if (this.onFinishScenario) {
|
||||
this.onFinishScenario();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderDesktopIcons(apps) {
|
||||
this.apps = apps;
|
||||
const iconsContainer = document.getElementById('desktop-icons');
|
||||
if (!iconsContainer) return;
|
||||
|
||||
iconsContainer.innerHTML = '';
|
||||
apps.forEach(app => {
|
||||
const iconEl = document.createElement('div');
|
||||
iconEl.className = 'desktop-icon';
|
||||
iconEl.dataset.appId = app.id;
|
||||
iconEl.innerHTML = `
|
||||
<div class="desktop-icon-img">${app.iconSvg}</div>
|
||||
<div class="desktop-icon-label">${app.name}</div>
|
||||
`;
|
||||
|
||||
iconEl.addEventListener('click', () => {
|
||||
iconsContainer.querySelectorAll('.desktop-icon').forEach(i => i.classList.remove('selected'));
|
||||
iconEl.classList.add('selected');
|
||||
});
|
||||
|
||||
iconEl.addEventListener('dblclick', () => {
|
||||
if (app.launch) app.launch();
|
||||
});
|
||||
|
||||
iconsContainer.appendChild(iconEl);
|
||||
});
|
||||
|
||||
// Also populate start menu app list
|
||||
const startAppList = document.getElementById('start-app-list');
|
||||
if (startAppList) {
|
||||
startAppList.innerHTML = '';
|
||||
apps.forEach(app => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'start-app-item';
|
||||
item.innerHTML = `
|
||||
${app.iconSvg}
|
||||
<div style="font-size:12px; font-weight:500;">${app.name}</div>
|
||||
`;
|
||||
item.addEventListener('click', () => {
|
||||
this.closeStartMenu();
|
||||
if (app.launch) app.launch();
|
||||
});
|
||||
startAppList.appendChild(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleStartMenu() {
|
||||
const isOpen = this.startMenu.classList.contains('open');
|
||||
if (isOpen) {
|
||||
this.closeStartMenu();
|
||||
} else {
|
||||
this.openStartMenu();
|
||||
}
|
||||
}
|
||||
|
||||
openStartMenu() {
|
||||
this.startMenu.classList.add('open');
|
||||
this.startBtn.classList.add('open');
|
||||
this.eventBus.emit('START_MENU_OPENED');
|
||||
}
|
||||
|
||||
closeStartMenu() {
|
||||
this.startMenu.classList.remove('open');
|
||||
this.startBtn.classList.remove('open');
|
||||
}
|
||||
|
||||
updateClock() {
|
||||
if (!this.clock) return;
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
const dateStr = now.toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
|
||||
this.clock.innerHTML = `
|
||||
<div class="time">${timeStr}</div>
|
||||
<div class="date">${dateStr}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* CyberSim OS - System Notifications Service & Web Audio Chime
|
||||
*/
|
||||
|
||||
export class NotificationService {
|
||||
constructor(containerElement) {
|
||||
this.container = containerElement || document.getElementById('notification-container');
|
||||
this.audioCtx = null;
|
||||
this.notifications = [];
|
||||
this.unreadCount = 0;
|
||||
}
|
||||
|
||||
initAudio() {
|
||||
if (!this.audioCtx) {
|
||||
try {
|
||||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
if (AudioContext) {
|
||||
this.audioCtx = new AudioContext();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Web Audio not supported or blocked:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
playChime(type = 'info') {
|
||||
try {
|
||||
this.initAudio();
|
||||
if (!this.audioCtx || this.audioCtx.state === 'suspended') {
|
||||
if (this.audioCtx) this.audioCtx.resume();
|
||||
}
|
||||
if (!this.audioCtx) return;
|
||||
|
||||
const osc = this.audioCtx.createOscillator();
|
||||
const gain = this.audioCtx.createGain();
|
||||
osc.connect(gain);
|
||||
gain.connect(this.audioCtx.destination);
|
||||
|
||||
const now = this.audioCtx.currentTime;
|
||||
if (type === 'danger' || type === 'warning') {
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(440, now);
|
||||
osc.frequency.exponentialRampToValueAtTime(220, now + 0.25);
|
||||
gain.gain.setValueAtTime(0.08, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.25);
|
||||
} else {
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(587.33, now); // D5
|
||||
osc.frequency.setValueAtTime(880.00, now + 0.08); // A5
|
||||
gain.gain.setValueAtTime(0.06, now);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + 0.25);
|
||||
osc.start(now);
|
||||
osc.stop(now + 0.25);
|
||||
}
|
||||
} catch (e) {
|
||||
// Audio playback fails silently if user hasn't interacted yet
|
||||
}
|
||||
}
|
||||
|
||||
show({ title, body, type = 'info', iconSvg = null, onClick = null, timeout = 6000 }) {
|
||||
this.playChime(type);
|
||||
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `cs-toast ${type}`;
|
||||
|
||||
const defaultIcon = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>`;
|
||||
|
||||
toast.innerHTML = `
|
||||
<div class="cs-toast-icon">${iconSvg || defaultIcon}</div>
|
||||
<div class="cs-toast-content">
|
||||
<div class="cs-toast-title">${title}</div>
|
||||
<div class="cs-toast-body">${body}</div>
|
||||
</div>
|
||||
<button class="cs-toast-close" title="Dismiss">×</button>
|
||||
`;
|
||||
|
||||
const closeBtn = toast.querySelector('.cs-toast-close');
|
||||
closeBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.dismiss(toast);
|
||||
});
|
||||
|
||||
if (onClick) {
|
||||
toast.addEventListener('click', () => {
|
||||
onClick();
|
||||
this.dismiss(toast);
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.container) {
|
||||
this.container = document.getElementById('notification-container');
|
||||
}
|
||||
if (this.container) {
|
||||
this.container.appendChild(toast);
|
||||
}
|
||||
|
||||
this.notifications.push({ title, body, type, time: new Date() });
|
||||
this.unreadCount++;
|
||||
this.updateTrayBadge();
|
||||
|
||||
if (timeout > 0) {
|
||||
setTimeout(() => {
|
||||
this.dismiss(toast);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
return toast;
|
||||
}
|
||||
|
||||
dismiss(toastElement) {
|
||||
if (!toastElement || !toastElement.parentNode) return;
|
||||
toastElement.style.opacity = '0';
|
||||
toastElement.style.transform = 'translateX(100%)';
|
||||
setTimeout(() => {
|
||||
if (toastElement.parentNode) {
|
||||
toastElement.parentNode.removeChild(toastElement);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
updateTrayBadge() {
|
||||
const badge = document.getElementById('tray-notify-badge');
|
||||
if (badge) {
|
||||
badge.style.display = this.unreadCount > 0 ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
|
||||
clearUnread() {
|
||||
this.unreadCount = 0;
|
||||
this.updateTrayBadge();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* CyberSim OS - Window Management Subsystem
|
||||
* Handles window creation, movement, sizing, z-index, minimize, maximize, and taskbar sync.
|
||||
*/
|
||||
|
||||
export class WindowManager {
|
||||
constructor(desktopElement, taskbarAppsElement) {
|
||||
this.desktop = desktopElement;
|
||||
this.taskbarApps = taskbarAppsElement;
|
||||
this.windows = new Map();
|
||||
this.activeWindow = null;
|
||||
this.baseZIndex = 10;
|
||||
this.currentZIndex = 10;
|
||||
this.cascadeOffset = 0;
|
||||
}
|
||||
|
||||
registerApp(appConfig) {
|
||||
// App definition registration
|
||||
}
|
||||
|
||||
createWindow({ id, title, iconSvg, width = 760, height = 520, x = null, y = null, bodyContent = '', onClose = null }) {
|
||||
if (this.windows.has(id)) {
|
||||
const win = this.windows.get(id);
|
||||
this.restoreWindow(id);
|
||||
this.focusWindow(id);
|
||||
return win;
|
||||
}
|
||||
|
||||
const defaultX = x !== null ? x : 80 + (this.cascadeOffset % 5) * 30;
|
||||
const defaultY = y !== null ? y : 40 + (this.cascadeOffset % 5) * 30;
|
||||
this.cascadeOffset++;
|
||||
|
||||
const winEl = document.createElement('div');
|
||||
winEl.className = 'cs-window active';
|
||||
winEl.id = `win-${id}`;
|
||||
winEl.style.width = `${width}px`;
|
||||
winEl.style.height = `${height}px`;
|
||||
winEl.style.left = `${defaultX}px`;
|
||||
winEl.style.top = `${defaultY}px`;
|
||||
winEl.style.zIndex = ++this.currentZIndex;
|
||||
|
||||
winEl.innerHTML = `
|
||||
<div class="cs-window-header" data-win-id="${id}">
|
||||
<div class="cs-window-title">
|
||||
${iconSvg || ''}
|
||||
<span>${title}</span>
|
||||
</div>
|
||||
<div class="cs-window-controls">
|
||||
<button class="cs-btn-win min" title="Minimize" data-action="min">
|
||||
<svg width="10" height="2" viewBox="0 0 10 2" fill="currentColor"><rect width="10" height="2"/></svg>
|
||||
</button>
|
||||
<button class="cs-btn-win max" title="Maximize" data-action="max">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="1" y="1" width="8" height="8"/></svg>
|
||||
</button>
|
||||
<button class="cs-btn-win close" title="Close" data-action="close">
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" stroke-width="1.5"><line x1="1" y1="1" x2="9" y2="9"/><line x1="9" y1="1" x2="1" y2="9"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cs-window-body" id="body-${id}">
|
||||
</div>
|
||||
`;
|
||||
|
||||
const bodyEl = winEl.querySelector(`#body-${id}`);
|
||||
if (typeof bodyContent === 'string') {
|
||||
bodyEl.innerHTML = bodyContent;
|
||||
} else if (bodyContent instanceof HTMLElement) {
|
||||
bodyEl.appendChild(bodyContent);
|
||||
}
|
||||
|
||||
this.desktop.appendChild(winEl);
|
||||
|
||||
// Create Taskbar Button
|
||||
const taskbarBtn = document.createElement('button');
|
||||
taskbarBtn.className = 'taskbar-btn active';
|
||||
taskbarBtn.id = `tb-btn-${id}`;
|
||||
taskbarBtn.innerHTML = `
|
||||
${iconSvg || ''}
|
||||
<span>${title}</span>
|
||||
`;
|
||||
taskbarBtn.addEventListener('click', () => {
|
||||
if (this.activeWindow === id && !winEl.classList.contains('minimized')) {
|
||||
this.minimizeWindow(id);
|
||||
} else {
|
||||
this.restoreWindow(id);
|
||||
this.focusWindow(id);
|
||||
}
|
||||
});
|
||||
this.taskbarApps.appendChild(taskbarBtn);
|
||||
|
||||
const winData = {
|
||||
id,
|
||||
title,
|
||||
element: winEl,
|
||||
bodyElement: bodyEl,
|
||||
taskbarBtn,
|
||||
isMaximized: false,
|
||||
isMinimized: false,
|
||||
prevGeometry: { x: defaultX, y: defaultY, w: width, h: height },
|
||||
onClose
|
||||
};
|
||||
|
||||
this.windows.set(id, winData);
|
||||
this.bindWindowEvents(winData);
|
||||
this.focusWindow(id);
|
||||
|
||||
return winData;
|
||||
}
|
||||
|
||||
bindWindowEvents(winData) {
|
||||
const { id, element } = winData;
|
||||
const header = element.querySelector('.cs-window-header');
|
||||
|
||||
// Click to Focus
|
||||
element.addEventListener('mousedown', () => {
|
||||
this.focusWindow(id);
|
||||
});
|
||||
|
||||
// Window Controls
|
||||
header.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.cs-btn-win');
|
||||
if (!btn) return;
|
||||
const action = btn.dataset.action;
|
||||
if (action === 'min') this.minimizeWindow(id);
|
||||
if (action === 'max') this.toggleMaximizeWindow(id);
|
||||
if (action === 'close') this.closeWindow(id);
|
||||
});
|
||||
|
||||
// Double-click header to maximize
|
||||
header.addEventListener('dblclick', (e) => {
|
||||
if (!e.target.closest('.cs-btn-win')) {
|
||||
this.toggleMaximizeWindow(id);
|
||||
}
|
||||
});
|
||||
|
||||
// Dragging Logic
|
||||
let isDragging = false;
|
||||
let startX, startY, initialLeft, initialTop;
|
||||
|
||||
header.addEventListener('mousedown', (e) => {
|
||||
if (e.target.closest('.cs-btn-win') || winData.isMaximized) return;
|
||||
isDragging = true;
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
initialLeft = element.offsetLeft;
|
||||
initialTop = element.offsetTop;
|
||||
|
||||
const onMouseMove = (moveEvent) => {
|
||||
if (!isDragging) return;
|
||||
const dx = moveEvent.clientX - startX;
|
||||
const dy = moveEvent.clientY - startY;
|
||||
|
||||
let newX = initialLeft + dx;
|
||||
let newY = initialTop + dy;
|
||||
|
||||
// Desktop bounds clamp
|
||||
const maxX = this.desktop.clientWidth - 80;
|
||||
const maxY = this.desktop.clientHeight - 40;
|
||||
newX = Math.max(-element.clientWidth + 100, Math.min(newX, maxX));
|
||||
newY = Math.max(0, Math.min(newY, maxY));
|
||||
|
||||
element.style.left = `${newX}px`;
|
||||
element.style.top = `${newY}px`;
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
isDragging = false;
|
||||
document.removeEventListener('mousemove', onMouseMove);
|
||||
document.removeEventListener('mouseup', onMouseUp);
|
||||
};
|
||||
|
||||
document.addEventListener('mousemove', onMouseMove);
|
||||
document.addEventListener('mouseup', onMouseUp);
|
||||
});
|
||||
}
|
||||
|
||||
focusWindow(id) {
|
||||
if (!this.windows.has(id)) return;
|
||||
this.activeWindow = id;
|
||||
|
||||
this.windows.forEach((win, winId) => {
|
||||
if (winId === id) {
|
||||
win.element.classList.add('active');
|
||||
win.taskbarBtn.classList.add('active');
|
||||
win.element.style.zIndex = ++this.currentZIndex;
|
||||
} else {
|
||||
win.element.classList.remove('active');
|
||||
win.taskbarBtn.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
minimizeWindow(id) {
|
||||
const win = this.windows.get(id);
|
||||
if (!win) return;
|
||||
win.isMinimized = true;
|
||||
win.element.classList.add('minimized');
|
||||
win.element.classList.remove('active');
|
||||
win.taskbarBtn.classList.remove('active');
|
||||
|
||||
if (this.activeWindow === id) {
|
||||
this.activeWindow = null;
|
||||
// Focus top-most visible window
|
||||
let topWin = null;
|
||||
let maxZ = -1;
|
||||
this.windows.forEach((w) => {
|
||||
if (!w.isMinimized && parseInt(w.element.style.zIndex || 0) > maxZ) {
|
||||
maxZ = parseInt(w.element.style.zIndex || 0);
|
||||
topWin = w.id;
|
||||
}
|
||||
});
|
||||
if (topWin) this.focusWindow(topWin);
|
||||
}
|
||||
}
|
||||
|
||||
restoreWindow(id) {
|
||||
const win = this.windows.get(id);
|
||||
if (!win) return;
|
||||
win.isMinimized = false;
|
||||
win.element.classList.remove('minimized');
|
||||
}
|
||||
|
||||
toggleMaximizeWindow(id) {
|
||||
const win = this.windows.get(id);
|
||||
if (!win) return;
|
||||
|
||||
if (win.isMaximized) {
|
||||
win.isMaximized = false;
|
||||
win.element.classList.remove('maximized');
|
||||
win.element.style.left = `${win.prevGeometry.x}px`;
|
||||
win.element.style.top = `${win.prevGeometry.y}px`;
|
||||
win.element.style.width = `${win.prevGeometry.w}px`;
|
||||
win.element.style.height = `${win.prevGeometry.h}px`;
|
||||
} else {
|
||||
win.prevGeometry = {
|
||||
x: win.element.offsetLeft,
|
||||
y: win.element.offsetTop,
|
||||
w: win.element.offsetWidth,
|
||||
h: win.element.offsetHeight
|
||||
};
|
||||
win.isMaximized = true;
|
||||
win.element.classList.add('maximized');
|
||||
}
|
||||
this.focusWindow(id);
|
||||
}
|
||||
|
||||
closeWindow(id) {
|
||||
const win = this.windows.get(id);
|
||||
if (!win) return;
|
||||
|
||||
if (win.onClose) {
|
||||
win.onClose();
|
||||
}
|
||||
|
||||
if (win.element.parentNode) {
|
||||
win.element.parentNode.removeChild(win.element);
|
||||
}
|
||||
if (win.taskbarBtn.parentNode) {
|
||||
win.taskbarBtn.parentNode.removeChild(win.taskbarBtn);
|
||||
}
|
||||
|
||||
this.windows.delete(id);
|
||||
if (this.activeWindow === id) {
|
||||
this.activeWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
getWindow(id) {
|
||||
return this.windows.get(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* CyberSim OS - Delayed Consequence Engine
|
||||
* Schedules and dispatches delayed consequences based on user behaviors.
|
||||
*/
|
||||
|
||||
export class ConsequenceEngine {
|
||||
constructor(eventBus, notificationsService, securityCenterApp) {
|
||||
this.eventBus = eventBus;
|
||||
this.notifications = notificationsService;
|
||||
this.securityCenter = securityCenterApp;
|
||||
this.scheduledEvents = [];
|
||||
this.activeTimer = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
// Listen for risky or notable behaviors to schedule realistic delayed consequences
|
||||
this.eventBus.on('NAV_FORM_SUBMITTED', (entry) => {
|
||||
if (entry.target === 'phish_login_form' || (entry.details && entry.details.url && entry.details.url.includes('nexac0re-portal.com'))) {
|
||||
this.schedule({
|
||||
delaySeconds: 35,
|
||||
id: 'consequence_credential_leak',
|
||||
name: 'Suspicious Account Activity Alert',
|
||||
execute: () => {
|
||||
if (this.securityCenter && this.securityCenter.addAlert) {
|
||||
this.securityCenter.addAlert({
|
||||
id: 'alert_unauthorized_sso',
|
||||
title: 'Security Alert: Anomalous Login from Unknown Location',
|
||||
severity: 'high',
|
||||
source: 'Identity Threat Detection',
|
||||
message: 'Multiple automated authentication attempts detected originating from an unrecognized IP address (198.51.100.42 - Eastern Europe) using recently submitted portal credentials. Password reset has been initiated.',
|
||||
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
});
|
||||
}
|
||||
if (this.notifications) {
|
||||
this.notifications.show({
|
||||
title: 'Security Center Alert',
|
||||
body: 'High Severity Alert: Anomalous login attempt detected on your account.',
|
||||
type: 'danger',
|
||||
timeout: 8000
|
||||
});
|
||||
}
|
||||
this.eventBus.emit('CONSEQUENCE_TRIGGERED', {
|
||||
target: 'consequence_credential_leak',
|
||||
details: { cause: 'Phishing credentials submitted' }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.eventBus.on('FILE_DOWNLOADED', (entry) => {
|
||||
if (entry.target === 'Invoice_88921_Receipt.zip') {
|
||||
this.schedule({
|
||||
delaySeconds: 25,
|
||||
id: 'consequence_suspicious_download',
|
||||
name: 'Antivirus File Warning',
|
||||
execute: () => {
|
||||
if (this.securityCenter && this.securityCenter.addAlert) {
|
||||
this.securityCenter.addAlert({
|
||||
id: 'alert_quarantine_zip',
|
||||
title: 'Endpoint Protection: Suspicious Archive Quarantined',
|
||||
severity: 'medium',
|
||||
source: 'Endpoint Threat Shield',
|
||||
message: 'Downloaded archive "Invoice_88921_Receipt.zip" contains suspicious executable payloads (PaymentReceipt.exe) masquerading as document files.',
|
||||
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
||||
});
|
||||
}
|
||||
if (this.notifications) {
|
||||
this.notifications.show({
|
||||
title: 'Endpoint Protection',
|
||||
body: 'Suspicious archive quarantined in Downloads folder.',
|
||||
type: 'warning',
|
||||
timeout: 7000
|
||||
});
|
||||
}
|
||||
this.eventBus.emit('CONSEQUENCE_TRIGGERED', {
|
||||
target: 'consequence_suspicious_download',
|
||||
details: { cause: 'Malicious zip downloaded' }
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
schedule(consequence) {
|
||||
const runAt = Date.now() + (consequence.delaySeconds * 1000);
|
||||
const item = { ...consequence, runAt, executed: false };
|
||||
this.scheduledEvents.push(item);
|
||||
|
||||
setTimeout(() => {
|
||||
if (!item.executed) {
|
||||
item.executed = true;
|
||||
item.execute();
|
||||
}
|
||||
}, consequence.delaySeconds * 1000);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.scheduledEvents = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* CyberSim OS - Event Bus & Behavioral Telemetry Logger
|
||||
* Records all user interactions with timestamps, simulation time, targets, and context.
|
||||
*/
|
||||
|
||||
export class EventBus {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
this.logs = [];
|
||||
this.startTime = Date.now();
|
||||
this.actionCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to an event
|
||||
*/
|
||||
on(event, callback) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, new Set());
|
||||
}
|
||||
this.listeners.get(event).add(callback);
|
||||
return () => this.off(event, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from an event
|
||||
*/
|
||||
off(event, callback) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).delete(callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event and log telemetry
|
||||
*/
|
||||
emit(event, data = {}) {
|
||||
const simSeconds = Math.floor((Date.now() - this.startTime) / 1000);
|
||||
const entry = {
|
||||
id: ++this.actionCounter,
|
||||
timestamp: new Date().toISOString(),
|
||||
simSeconds,
|
||||
event,
|
||||
target: data.target || null,
|
||||
details: data.details || {},
|
||||
category: data.category || 'general'
|
||||
};
|
||||
|
||||
this.logs.push(entry);
|
||||
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach(cb => {
|
||||
try {
|
||||
cb(entry);
|
||||
} catch (err) {
|
||||
console.error(`Error in event listener for ${event}:`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Also trigger wildcard listeners
|
||||
if (this.listeners.has('*')) {
|
||||
this.listeners.get('*').forEach(cb => cb(entry));
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query recorded telemetry logs
|
||||
*/
|
||||
getLogs() {
|
||||
return [...this.logs];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find actions matching a predicate or event name
|
||||
*/
|
||||
findActions(predicate) {
|
||||
if (typeof predicate === 'string') {
|
||||
return this.logs.filter(l => l.event === predicate);
|
||||
}
|
||||
return this.logs.filter(predicate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific action on a target occurred
|
||||
*/
|
||||
hasAction(event, target = null) {
|
||||
return this.logs.some(l => {
|
||||
if (target !== null) {
|
||||
return l.event === event && l.target === target;
|
||||
}
|
||||
return l.event === event;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset logs
|
||||
*/
|
||||
reset() {
|
||||
this.logs = [];
|
||||
this.startTime = Date.now();
|
||||
this.actionCounter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
export const globalEventBus = new EventBus();
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* CyberSim OS - Main Bootstrapper & Simulation Lifecycle Orchestrator
|
||||
*/
|
||||
|
||||
import { globalEventBus } from './engine/event_bus.js';
|
||||
import { ConsequenceEngine } from './engine/consequence.js';
|
||||
import { calculateScenarioFingerprint } from './scenario/fingerprint.js';
|
||||
import { referenceScenario1 } from './scenario/scenario_ref1.js';
|
||||
import { WindowManager } from './core/window_manager.js';
|
||||
import { DesktopShell } from './core/desktop.js';
|
||||
import { NotificationService } from './core/notifications.js';
|
||||
|
||||
import { InlookApp } from './apps/inlook.js';
|
||||
import { NavigatorApp } from './apps/navigator.js';
|
||||
import { FilesApp } from './apps/files.js';
|
||||
import { DocViewerApp } from './apps/docviewer.js';
|
||||
import { SecurityCenterApp } from './apps/security_center.js';
|
||||
|
||||
import { BehavioralScorer } from './scoring/scorer.js';
|
||||
import { AfterActionReport } from './scoring/aar.js';
|
||||
import { CertificateGenerator } from './cert/cert_generator.js';
|
||||
|
||||
class CyberSimEngine {
|
||||
constructor() {
|
||||
this.scenario = referenceScenario1;
|
||||
this.scenarioFingerprint = null;
|
||||
this.eventBus = globalEventBus;
|
||||
}
|
||||
|
||||
async start() {
|
||||
console.log('[CyberSim OS] Booting enterprise desktop simulation...');
|
||||
|
||||
// Calculate cryptographic scenario fingerprint
|
||||
this.scenarioFingerprint = await calculateScenarioFingerprint(this.scenario);
|
||||
console.log(`[CyberSim OS] Scenario Fingerprint (SHA-256): ${this.scenarioFingerprint}`);
|
||||
|
||||
// Initialize UI Containers
|
||||
const desktopEl = document.getElementById('desktop');
|
||||
const taskbarAppsEl = document.getElementById('taskbar-apps');
|
||||
const startMenuEl = document.getElementById('start-menu');
|
||||
const startBtnEl = document.getElementById('start-btn');
|
||||
const clockEl = document.getElementById('taskbar-clock');
|
||||
|
||||
// Initialize Core Services
|
||||
this.notifications = new NotificationService();
|
||||
this.wm = new WindowManager(desktopEl, taskbarAppsEl);
|
||||
|
||||
// Initialize Apps
|
||||
this.docViewer = new DocViewerApp({
|
||||
windowManager: this.wm,
|
||||
eventBus: this.eventBus
|
||||
});
|
||||
|
||||
this.files = new FilesApp({
|
||||
windowManager: this.wm,
|
||||
eventBus: this.eventBus,
|
||||
notifications: this.notifications,
|
||||
scenario: this.scenario,
|
||||
onOpenFile: (file) => this.docViewer.openDocument(file)
|
||||
});
|
||||
|
||||
this.navigator = new NavigatorApp({
|
||||
windowManager: this.wm,
|
||||
eventBus: this.eventBus,
|
||||
notifications: this.notifications,
|
||||
scenario: this.scenario
|
||||
});
|
||||
|
||||
this.securityCenter = new SecurityCenterApp({
|
||||
windowManager: this.wm,
|
||||
eventBus: this.eventBus
|
||||
});
|
||||
|
||||
this.inlook = new InlookApp({
|
||||
windowManager: this.wm,
|
||||
eventBus: this.eventBus,
|
||||
notifications: this.notifications,
|
||||
scenario: this.scenario,
|
||||
onNavigateUrl: (url) => this.navigator.launch(url),
|
||||
onOpenDoc: (file) => this.docViewer.openDocument(file),
|
||||
onFileDownloaded: (file) => this.files.addFile(file)
|
||||
});
|
||||
|
||||
// Initialize Delayed Consequence Engine
|
||||
this.consequences = new ConsequenceEngine(this.eventBus, this.notifications, this.securityCenter);
|
||||
|
||||
// Initialize Desktop Shell
|
||||
this.desktop = new DesktopShell({
|
||||
desktopElement: desktopEl,
|
||||
startMenuElement: startMenuEl,
|
||||
startBtnElement: startBtnEl,
|
||||
clockElement: clockEl,
|
||||
eventBus: this.eventBus,
|
||||
onFinishScenario: () => this.finishScenario()
|
||||
});
|
||||
|
||||
// Register Desktop Apps
|
||||
const registeredApps = [
|
||||
{
|
||||
id: 'inlook',
|
||||
name: 'Inlook Mail',
|
||||
iconSvg: this.inlook.getIconSvg(),
|
||||
launch: () => this.inlook.launch()
|
||||
},
|
||||
{
|
||||
id: 'navigator',
|
||||
name: 'Navigator',
|
||||
iconSvg: this.navigator.getIconSvg(),
|
||||
launch: () => this.navigator.launch()
|
||||
},
|
||||
{
|
||||
id: 'files',
|
||||
name: 'Files',
|
||||
iconSvg: this.files.getIconSvg(),
|
||||
launch: () => this.files.launch()
|
||||
},
|
||||
{
|
||||
id: 'security_center',
|
||||
name: 'Security Center',
|
||||
iconSvg: this.securityCenter.getIconSvg(),
|
||||
launch: () => this.securityCenter.launch()
|
||||
},
|
||||
{
|
||||
id: 'docviewer',
|
||||
name: 'Doc Viewer',
|
||||
iconSvg: this.docViewer.getIconSvg(),
|
||||
launch: () => {
|
||||
const firstDoc = this.scenario.files[0];
|
||||
if (firstDoc) this.docViewer.openDocument(firstDoc);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'verify_cert',
|
||||
name: 'Verify Cert',
|
||||
iconSvg: `<svg viewBox="0 0 24 24" fill="none" stroke="#8b5cf6" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>`,
|
||||
launch: () => {
|
||||
window.open('verify.html', '_blank');
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
this.desktop.renderDesktopIcons(registeredApps);
|
||||
|
||||
// Expose runtime global for event triggers
|
||||
window.CyberSimOS = {
|
||||
engine: this,
|
||||
navigator: this.navigator,
|
||||
inlook: this.inlook,
|
||||
files: this.files,
|
||||
securityCenter: this.securityCenter,
|
||||
docViewer: this.docViewer,
|
||||
handlePhishSubmit: (form) => this.navigator.handlePhishFormSubmit(form),
|
||||
finishSimulation: () => this.finishScenario()
|
||||
};
|
||||
|
||||
// Emit Scenario Start Event
|
||||
this.eventBus.emit('SCENARIO_STARTED', {
|
||||
target: this.scenario.scenarioId,
|
||||
details: {
|
||||
title: this.scenario.title,
|
||||
fingerprint: this.scenarioFingerprint
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-launch Inlook and show orientation toast
|
||||
setTimeout(() => {
|
||||
this.inlook.launch();
|
||||
this.notifications.show({
|
||||
title: 'NexaCore Orientation',
|
||||
body: 'Welcome Jordan! Check Inlook for initial tasks from Morgan Chen.',
|
||||
type: 'info',
|
||||
timeout: 8000
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
|
||||
finishScenario() {
|
||||
this.eventBus.emit('SCENARIO_FINISHED', {
|
||||
target: this.scenario.scenarioId
|
||||
});
|
||||
|
||||
const logs = this.eventBus.getLogs();
|
||||
const scorer = new BehavioralScorer(this.scenario, logs);
|
||||
const scoreResult = scorer.evaluate();
|
||||
|
||||
const aar = new AfterActionReport({
|
||||
scenario: this.scenario,
|
||||
scoreResult,
|
||||
onClaimCertificate: () => {
|
||||
const certGen = new CertificateGenerator(this.scenario, scoreResult, this.scenarioFingerprint);
|
||||
certGen.showCertificateModal(this.scenario.learner.name);
|
||||
},
|
||||
onRestart: () => {
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
aar.show();
|
||||
}
|
||||
}
|
||||
|
||||
// Boot on DOM Ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const sim = new CyberSimEngine();
|
||||
sim.start();
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* CyberSim OS - Scenario Fingerprinting Utility
|
||||
* Cryptographically hashes immutable scenario definitions using native Web Crypto API (SHA-256).
|
||||
*/
|
||||
|
||||
export async function calculateScenarioFingerprint(scenarioData) {
|
||||
try {
|
||||
// Create canonical representation excluding volatile runtime state
|
||||
const canonicalObject = {
|
||||
scenarioId: scenarioData.scenarioId,
|
||||
version: scenarioData.version,
|
||||
company: scenarioData.company,
|
||||
emails: scenarioData.emails.map(e => ({
|
||||
id: e.id,
|
||||
sender: e.sender,
|
||||
rfcSender: e.rfcSender,
|
||||
subject: e.subject,
|
||||
body: e.body,
|
||||
isThreat: !!e.isThreat,
|
||||
isFalseFlag: !!e.isFalseFlag,
|
||||
links: e.links || [],
|
||||
attachments: e.attachments || []
|
||||
})),
|
||||
files: scenarioData.files.map(f => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
type: f.type,
|
||||
folder: f.folder
|
||||
})),
|
||||
pages: scenarioData.pages.map(p => ({
|
||||
url: p.url,
|
||||
title: p.title
|
||||
})),
|
||||
threats: scenarioData.threats,
|
||||
falseFlags: scenarioData.falseFlags
|
||||
};
|
||||
|
||||
const canonicalJson = JSON.stringify(canonicalObject, Object.keys(canonicalObject).sort());
|
||||
const msgBuffer = new TextEncoder().encode(canonicalJson);
|
||||
const hashBuffer = await window.crypto.subtle.digest('SHA-256', msgBuffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
return hashHex;
|
||||
} catch (err) {
|
||||
console.error('Error calculating scenario fingerprint:', err);
|
||||
// Fallback hash
|
||||
return 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
/**
|
||||
* CyberSim OS - Reference Scenario 1
|
||||
* "Day One at NexaCore Technologies - Operational Shift & Security Awareness"
|
||||
*/
|
||||
|
||||
export const referenceScenario1 = {
|
||||
scenarioId: 'nexacore-shift-1',
|
||||
title: 'Operational Shift & Security Awareness',
|
||||
version: '1.0.0',
|
||||
description: 'Begin your work shift at NexaCore Technologies. Review requested budget forecasts, handle communications, investigate anomalies, and make sound security decisions.',
|
||||
learner: {
|
||||
name: 'Jordan Taylor',
|
||||
role: 'Financial Operations Specialist',
|
||||
email: '[email protected]',
|
||||
department: 'Finance & Accounting'
|
||||
},
|
||||
company: {
|
||||
name: 'NexaCore Technologies',
|
||||
domain: 'nexacore.internal',
|
||||
intranetUrl: 'http://intranet.nexacore.internal'
|
||||
},
|
||||
|
||||
// Initial Scenario Goals
|
||||
objectives: [
|
||||
'Review the welcome email and tasks from your manager (Morgan Chen)',
|
||||
'Review the Q3 Budget Forecast spreadsheet in your Documents folder',
|
||||
'Familiarize yourself with the NexaCore Cybersecurity Policy',
|
||||
'Review and handle any incoming communications appropriately',
|
||||
'Investigate any suspicious or unusual events using company tools',
|
||||
'Submit incident reports via Security Center if genuine threats are detected'
|
||||
],
|
||||
|
||||
// Threat & False Flag Declarations
|
||||
threats: [
|
||||
{
|
||||
id: 'threat_phish_pwreset',
|
||||
type: 'credential_phishing',
|
||||
name: 'Fake IT Password Reset Phishing Email',
|
||||
sourceEmailId: 'email_phish_pwreset',
|
||||
indicators: [
|
||||
'Sender RFC domain is @nexac0re-portal.com (typosquatted with zero)',
|
||||
'Display text shows legitimate intranet URL but href links to external http://login-nexac0re-portal.com',
|
||||
'Artificial urgency threatening immediate account deactivation',
|
||||
'Direct violation of NexaCore Security Policy Section 3'
|
||||
],
|
||||
correctAction: 'inspect_and_report',
|
||||
dangerousActions: ['click_link', 'submit_credentials']
|
||||
},
|
||||
{
|
||||
id: 'threat_malicious_invoice',
|
||||
type: 'malicious_attachment',
|
||||
name: 'Unsolicited Vendor Invoice with Executable Payload',
|
||||
sourceEmailId: 'email_malicious_invoice',
|
||||
indicators: [
|
||||
'Vendor Apex Supply Partners is NOT on the approved Company Shared Vendor Directory',
|
||||
'Attachment is an executable archive (Invoice_88921_Receipt.zip)',
|
||||
'Urgent legal and penalty threats for non-existent service invoice'
|
||||
],
|
||||
correctAction: 'inspect_and_report',
|
||||
dangerousActions: ['download_attachment', 'open_executable']
|
||||
}
|
||||
],
|
||||
|
||||
falseFlags: [
|
||||
{
|
||||
id: 'flag_legit_mfa',
|
||||
type: 'legitimate_security_notice',
|
||||
name: 'CISO Company-Wide MFA Security Policy Announcement',
|
||||
sourceEmailId: 'email_legit_mfa',
|
||||
indicators: [
|
||||
'Sender is genuinely [email protected] (matches Intranet Employee Directory)',
|
||||
'Does NOT ask for password entry or direct link login',
|
||||
'Instructs employees to navigate independently to the Intranet IT Security page',
|
||||
'Confirmatory announcement is present on the company Intranet news feed'
|
||||
],
|
||||
correctAction: 'verify_and_retain',
|
||||
incorrectAction: 'report_as_phishing'
|
||||
},
|
||||
{
|
||||
id: 'flag_coworker_spreadsheet',
|
||||
type: 'routine_document_share',
|
||||
name: 'Coworker Financial Inquiry from Sarah Jenkins',
|
||||
sourceEmailId: 'email_coworker_req',
|
||||
indicators: [
|
||||
'Sender is verified internal colleague [email protected]',
|
||||
'Pertains directly to the assigned Q3 Budget Forecast task',
|
||||
'No deceptive links or suspicious payloads'
|
||||
],
|
||||
correctAction: 'reply_or_continue_work',
|
||||
incorrectAction: 'report_as_phishing'
|
||||
}
|
||||
],
|
||||
|
||||
// Simulated Inlook Emails
|
||||
emails: [
|
||||
{
|
||||
id: 'email_welcome',
|
||||
folder: 'inbox',
|
||||
sender: 'Morgan Chen (VP of Finance)',
|
||||
rfcSender: '[email protected]',
|
||||
recipient: '[email protected]',
|
||||
subject: 'Welcome to the team - Tasks for today',
|
||||
date: '08:45 AM',
|
||||
unread: true,
|
||||
starred: true,
|
||||
body: `Hi Jordan,
|
||||
|
||||
Welcome to the NexaCore Finance team!
|
||||
|
||||
For your shift today, please make sure to complete the following:
|
||||
1. Open the Files app and review the "Q3_Budget_Forecast.xlsx" document in your Documents folder. Sarah Jenkins in Accounting may ask you for an update on the operating margins.
|
||||
2. Read through the "NexaCore_Cyber_Security_Policy_v4.pdf" in your Documents folder to ensure you know our reporting protocols.
|
||||
3. Check the company Intranet for any updates or staff listings.
|
||||
|
||||
Let me know if you run into any questions. Have a great shift!
|
||||
|
||||
Best,
|
||||
Morgan Chen
|
||||
VP of Finance | NexaCore Technologies`,
|
||||
links: [],
|
||||
attachments: []
|
||||
},
|
||||
{
|
||||
id: 'email_phish_pwreset',
|
||||
folder: 'inbox',
|
||||
sender: 'NexaCore IT Helpdesk',
|
||||
rfcSender: '[email protected]',
|
||||
recipient: '[email protected]',
|
||||
subject: 'URGENT: Mandatory Single Sign-On (SSO) Migration & Password Verification',
|
||||
date: '09:05 AM',
|
||||
unread: true,
|
||||
starred: false,
|
||||
isThreat: true,
|
||||
threatId: 'threat_phish_pwreset',
|
||||
body: `Attention NexaCore Employee,
|
||||
|
||||
Our enterprise IT infrastructure is undergoing a mandatory Single Sign-On (SSO) security migration. All active user accounts must be verified within the next 2 hours to avoid temporary account suspension and loss of network access.
|
||||
|
||||
Please confirm your employee credentials immediately using our secured identity portal:
|
||||
|
||||
👉 Verify Credentials: <a class="inlook-msg-link" data-url="http://login-nexac0re-portal.com/auth/login" data-display="https://intranet.nexacore.internal/sso-verification">https://intranet.nexacore.internal/sso-verification</a>
|
||||
|
||||
Failure to verify before 11:00 AM will require managerial intervention to re-enable your workstation.
|
||||
|
||||
IT Support Services
|
||||
NexaCore Global Infrastructure`,
|
||||
links: [
|
||||
{
|
||||
displayText: 'https://intranet.nexacore.internal/sso-verification',
|
||||
actualUrl: 'http://login-nexac0re-portal.com/auth/login',
|
||||
isPhishing: true
|
||||
}
|
||||
],
|
||||
attachments: []
|
||||
},
|
||||
{
|
||||
id: 'email_legit_mfa',
|
||||
folder: 'inbox',
|
||||
sender: 'Alex Rivera (CISO)',
|
||||
rfcSender: '[email protected]',
|
||||
recipient: '[email protected]',
|
||||
subject: 'Information Security Notice: Upcoming Company-Wide MFA Policy',
|
||||
date: '09:12 AM',
|
||||
unread: true,
|
||||
starred: false,
|
||||
isFalseFlag: true,
|
||||
falseFlagId: 'flag_legit_mfa',
|
||||
body: `Hello Team,
|
||||
|
||||
As part of our continuous cybersecurity hardening, NexaCore Information Security will be rolling out hardware security keys and updated Multi-Factor Authentication (MFA) protocols starting next week.
|
||||
|
||||
Important Safety Reminder:
|
||||
- We will NEVER send you an email link requesting your password or authentication code.
|
||||
- To check your registered devices or read the full deployment schedule, visit our IT Security page directly on the company Intranet (http://intranet.nexacore.internal/it-security).
|
||||
|
||||
Thank you for helping keep NexaCore secure.
|
||||
|
||||
Sincerely,
|
||||
Alex Rivera
|
||||
Chief Information Security Officer
|
||||
NexaCore Technologies`,
|
||||
links: [
|
||||
{
|
||||
displayText: 'http://intranet.nexacore.internal/it-security',
|
||||
actualUrl: 'http://intranet.nexacore.internal/it-security',
|
||||
isPhishing: false
|
||||
}
|
||||
],
|
||||
attachments: []
|
||||
},
|
||||
{
|
||||
id: 'email_malicious_invoice',
|
||||
folder: 'inbox',
|
||||
sender: 'Apex Supply Partners Billing',
|
||||
rfcSender: '[email protected]',
|
||||
recipient: '[email protected]',
|
||||
subject: 'FINAL DEMAND: Overdue Server Hardware Invoice #INV-88921',
|
||||
date: '09:20 AM',
|
||||
unread: true,
|
||||
starred: false,
|
||||
isThreat: true,
|
||||
threatId: 'threat_malicious_invoice',
|
||||
body: `Attention Finance Department,
|
||||
|
||||
Invoice #INV-88921 for the recent delivery of high-density server chassis is now 45 days past due. A late assessment penalty has been added to the balance.
|
||||
|
||||
Please review the attached itemized payment statement and remit payment immediately to avoid collection proceedings:
|
||||
|
||||
Attachment: Invoice_88921_Receipt.zip
|
||||
|
||||
Regards,
|
||||
Accounting & Recovery Division
|
||||
Apex Supply Partners Ltd.`,
|
||||
links: [],
|
||||
attachments: [
|
||||
{
|
||||
name: 'Invoice_88921_Receipt.zip',
|
||||
size: '342 KB',
|
||||
isMalicious: true,
|
||||
type: 'archive'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'email_coworker_req',
|
||||
folder: 'inbox',
|
||||
sender: 'Sarah Jenkins (Accounting)',
|
||||
rfcSender: '[email protected]',
|
||||
recipient: '[email protected]',
|
||||
subject: 'Quick question on Q3 Budget Forecast',
|
||||
date: '09:35 AM',
|
||||
unread: true,
|
||||
starred: false,
|
||||
isFalseFlag: true,
|
||||
falseFlagId: 'flag_coworker_spreadsheet',
|
||||
body: `Hi Jordan,
|
||||
|
||||
Hope your first morning is going smoothly!
|
||||
|
||||
When you get a chance to inspect the Q3 Budget Forecast spreadsheet in your Documents folder, could you double-check the projected Server & Cloud Infrastructure costs on Row 4? Morgan mentioned we might need to adjust the contingency buffer.
|
||||
|
||||
Thanks a lot!
|
||||
Sarah Jenkins
|
||||
Senior Financial Analyst`,
|
||||
links: [],
|
||||
attachments: []
|
||||
}
|
||||
],
|
||||
|
||||
// Virtual Filesystem Content
|
||||
files: [
|
||||
{
|
||||
id: 'file_budget',
|
||||
name: 'Q3_Budget_Forecast.xlsx',
|
||||
type: 'spreadsheet',
|
||||
folder: 'Documents',
|
||||
size: '48 KB',
|
||||
date: '2026-08-20',
|
||||
content: {
|
||||
title: 'NexaCore Technologies - Q3 Budget Forecast (Draft)',
|
||||
headers: ['Category', 'Q1 Actual', 'Q2 Actual', 'Q3 Projected', 'Variance %'],
|
||||
rows: [
|
||||
['Server & Cloud Infrastructure', '$142,000', '$155,000', '$168,000', '+8.4%'],
|
||||
['Research & Robotics Hardware', '$280,000', '$310,000', '$325,000', '+4.8%'],
|
||||
['Software Licenses & SaaS', '$64,000', '$68,000', '$71,000', '+4.4%'],
|
||||
['Security Audits & Compliance', '$35,000', '$40,000', '$45,000', '+12.5%'],
|
||||
['Total Operating Expenditures', '$521,000', '$573,000', '$609,000', '+6.2%']
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'file_sec_policy',
|
||||
name: 'NexaCore_Cyber_Security_Policy_v4.pdf',
|
||||
type: 'pdf',
|
||||
folder: 'Documents',
|
||||
size: '124 KB',
|
||||
date: '2026-08-15',
|
||||
content: {
|
||||
title: 'NexaCore Information Security Policy (v4.2)',
|
||||
sections: [
|
||||
{
|
||||
heading: '1. Purpose & Scope',
|
||||
text: 'This policy defines mandatory baseline security procedures for all NexaCore personnel handling digital communications, documents, and credentials.'
|
||||
},
|
||||
{
|
||||
heading: '2. Email & Phishing Defense',
|
||||
text: 'All employees must inspect the true RFC sender address before trusting emails requesting urgent actions. NexaCore IT will never distribute links requesting direct password entry. Any email utilizing mismatched link targets or urgent threats must be reported immediately via Security Center.'
|
||||
},
|
||||
{
|
||||
heading: '3. Vendor & Payment Verification',
|
||||
text: 'Prior to opening attachments or processing invoices from third parties, employees must cross-reference the vendor against the Approved Vendor Directory in the Company Shared folder. Unsolicited invoices containing executable or compressed files must be treated as malicious.'
|
||||
},
|
||||
{
|
||||
heading: '4. Reporting Procedures',
|
||||
text: 'Use the Security Center application or the "Report Suspicious Message" button in Inlook to escalate threats to the Security Operations Center (SOC). Do not forward phishing emails to colleagues.'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'file_vendor_dir',
|
||||
name: 'Vendor_Directory.pdf',
|
||||
type: 'pdf',
|
||||
folder: 'Company Shared',
|
||||
size: '88 KB',
|
||||
date: '2026-08-10',
|
||||
content: {
|
||||
title: 'NexaCore Approved Vendor Directory (2026)',
|
||||
sections: [
|
||||
{
|
||||
heading: 'Approved Hardware & Cloud Vendors',
|
||||
text: 'The following vendors are authorized for procurement and billing:'
|
||||
}
|
||||
],
|
||||
table: {
|
||||
headers: ['Vendor Name', 'Vendor Code', 'Contact Email', 'Status'],
|
||||
rows: [
|
||||
['Titan Cloud Systems', 'VND-104', '[email protected]', 'Active'],
|
||||
['Quantum Edge Hardware', 'VND-209', '[email protected]', 'Active'],
|
||||
['NexaLogistics Global', 'VND-315', '[email protected]', 'Active'],
|
||||
['CyberShield Auditing LLC', 'VND-402', '[email protected]', 'Active']
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
// Simulated Web Pages for Navigator
|
||||
pages: [
|
||||
{
|
||||
url: 'http://intranet.nexacore.internal',
|
||||
title: 'NexaCore Intranet - Home',
|
||||
isSecure: true,
|
||||
content: `
|
||||
<div class="webpage-intranet">
|
||||
<div class="intranet-header">
|
||||
<div class="intranet-title">NexaCore Enterprise Intranet</div>
|
||||
<div style="font-size:12px; color:#64748b;">Monday, August 24, 2026</div>
|
||||
</div>
|
||||
<div class="intranet-grid">
|
||||
<div>
|
||||
<div class="intranet-card" style="margin-bottom:16px;">
|
||||
<h3>Company Announcements</h3>
|
||||
<div style="font-size:12px; color:#334155; line-height:1.5;">
|
||||
<p><strong>🔒 Security Hardening:</strong> As announced by CISO Alex Rivera, company-wide MFA upgrades are underway. Remember to review our security policy in your Documents folder.</p>
|
||||
<p style="margin-top:8px;"><strong>📊 Q3 Financial Review:</strong> Department budget projections are being finalized this week. Contact Morgan Chen with any queries.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="intranet-card">
|
||||
<h3>Quick Resources</h3>
|
||||
<ul style="padding-left:18px; font-size:12px; color:#2563eb; line-height:1.8;">
|
||||
<li><a href="http://intranet.nexacore.internal/it-security" style="color:#2563eb;">IT Security Policies & Verified Alerts</a></li>
|
||||
<li><a href="http://intranet.nexacore.internal/directory" style="color:#2563eb;">Full Employee Directory</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="intranet-card">
|
||||
<h3>Key Contacts</h3>
|
||||
<table class="directory-table">
|
||||
<tr><th>Name</th><th>Role</th><th>Email</th></tr>
|
||||
<tr><td>Morgan Chen</td><td>VP Finance</td><td>morgan.chen@nexacore.internal</td></tr>
|
||||
<tr><td>Alex Rivera</td><td>CISO</td><td>alex.rivera@nexacore.internal</td></tr>
|
||||
<tr><td>Sarah Jenkins</td><td>Sr Accountant</td><td>sarah.jenkins@nexacore.internal</td></tr>
|
||||
<tr><td>IT Helpdesk</td><td>Support</td><td>it-helpdesk@nexacore.internal</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
url: 'http://intranet.nexacore.internal/it-security',
|
||||
title: 'IT Security Department - Policy & Notices',
|
||||
isSecure: true,
|
||||
content: `
|
||||
<div class="webpage-intranet">
|
||||
<div class="intranet-header">
|
||||
<div class="intranet-title">IT Security & Compliance Portal</div>
|
||||
<a href="http://intranet.nexacore.internal" style="font-size:12px; color:#2563eb;">← Back to Intranet</a>
|
||||
</div>
|
||||
<div class="intranet-card" style="margin-bottom:16px;">
|
||||
<h3>Official Notice: Hardware MFA Rollout (CISO Alex Rivera)</h3>
|
||||
<p style="font-size:12px; color:#334155; line-height:1.5;">
|
||||
NexaCore is transitioning all personnel to hardware FIDO2 keys and authenticator apps.
|
||||
<strong>Official Reminder:</strong> NexaCore IT will NEVER email you asking for your password or sending direct credential reset links.
|
||||
</p>
|
||||
</div>
|
||||
<div class="intranet-card">
|
||||
<h3>Known Threat Advisory: Phishing Campaigns Targeting NexaCore</h3>
|
||||
<p style="font-size:12px; color:#dc2626; line-height:1.5;">
|
||||
⚠️ Be aware of external lookalike domains such as <code>nexac0re-portal.com</code> attempting credential harvesting. Always verify the address bar before entering any information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
},
|
||||
{
|
||||
url: 'http://login-nexac0re-portal.com/auth/login',
|
||||
title: 'NexaCore SSO - Single Sign-On Authentication',
|
||||
isSecure: false,
|
||||
isPhishing: true,
|
||||
content: `
|
||||
<div class="webpage-phish">
|
||||
<div class="phish-card">
|
||||
<div class="phish-logo">NexaCore Identity SSO</div>
|
||||
<div class="phish-subtitle">Enter your employee credentials to verify your account</div>
|
||||
<form id="phish-login-form" onsubmit="event.preventDefault(); window.CyberSimOS.handlePhishSubmit(this);">
|
||||
<div class="cs-form-group">
|
||||
<label class="cs-form-label">Employee ID / Email</label>
|
||||
<input type="text" id="phish_user" class="cs-input" placeholder="e.g. [email protected]" required>
|
||||
</div>
|
||||
<div class="cs-form-group">
|
||||
<label class="cs-form-label">Password</label>
|
||||
<input type="password" id="phish_pass" class="cs-input" placeholder="••••••••••••" required>
|
||||
</div>
|
||||
<button type="submit" class="cs-btn cs-btn-primary" style="width:100%; margin-top:8px;">Sign In & Verify Account</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* CyberSim OS - After-Action Report (AAR) Modal Interface
|
||||
*/
|
||||
|
||||
export class AfterActionReport {
|
||||
constructor({ scenario, scoreResult, onClaimCertificate, onRestart }) {
|
||||
this.scenario = scenario;
|
||||
this.scoreResult = scoreResult;
|
||||
this.onClaimCertificate = onClaimCertificate;
|
||||
this.onRestart = onRestart;
|
||||
}
|
||||
|
||||
show() {
|
||||
const existing = document.getElementById('aar-modal-overlay');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'cs-modal-overlay';
|
||||
overlay.id = 'aar-modal-overlay';
|
||||
overlay.style.zIndex = '9000';
|
||||
|
||||
const r = this.scoreResult;
|
||||
const cats = r.categories;
|
||||
|
||||
overlay.innerHTML = `
|
||||
<div class="cs-modal" style="max-width:680px;">
|
||||
<div class="cs-modal-header">
|
||||
<div class="cs-modal-title">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>
|
||||
CyberSim OS - Behavioral After-Action Report (AAR)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cs-modal-body" style="padding:22px;">
|
||||
<div class="aar-header-summary">
|
||||
<div>
|
||||
<div style="font-size:11px; color:#64748b; text-transform:uppercase; font-weight:700; letter-spacing:0.05em;">Overall Assessment</div>
|
||||
<div class="aar-score-badge">${r.totalScore} <span style="font-size:16px; color:#64748b; font-weight:500;">/ 100</span></div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="aar-result-pill ${r.isPassed ? 'pass' : 'fail'}">
|
||||
${r.isPassed ? '✓ Passed (Threshold: 80)' : '✕ Needs Review (Threshold: 80)'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h4 style="font-size:12px; font-weight:700; text-transform:uppercase; color:#475569; margin-bottom:12px;">Competency Dimension Scores</h4>
|
||||
|
||||
<div style="display:flex; flex-direction:column; gap:10px; margin-bottom:20px;">
|
||||
${Object.keys(cats).map(key => {
|
||||
const c = cats[key];
|
||||
const pct = Math.round((c.score / c.max) * 100);
|
||||
let colorClass = 'success';
|
||||
if (pct < 60) colorClass = 'danger';
|
||||
else if (pct < 80) colorClass = 'warning';
|
||||
|
||||
return `
|
||||
<div class="aar-category-row">
|
||||
<div class="aar-cat-header">
|
||||
<span>${c.label}</span>
|
||||
<span><strong>${c.score}</strong> / ${c.max} (${pct}%)</span>
|
||||
</div>
|
||||
<div class="aar-progress-bg">
|
||||
<div class="aar-progress-fill ${colorClass}" style="width:${pct}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
|
||||
<h4 style="font-size:12px; font-weight:700; text-transform:uppercase; color:#475569; margin-bottom:8px;">Pedagogical Feedback & Observations</h4>
|
||||
<div class="aar-feedback-box">
|
||||
<ul style="padding-left:18px; line-height:1.6;">
|
||||
${r.feedback.map(f => `<li>${f}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cs-modal-footer">
|
||||
<button class="cs-btn" id="btn-aar-restart">Restart Simulation</button>
|
||||
${r.isPassed ? `
|
||||
<button class="cs-btn cs-btn-primary" id="btn-aar-cert">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="8" r="7"/><polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/></svg>
|
||||
Claim Verifiable Certificate
|
||||
</button>
|
||||
` : `
|
||||
<button class="cs-btn cs-btn-danger" id="btn-aar-retry">Retry Simulation</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
overlay.querySelector('#btn-aar-restart').addEventListener('click', () => {
|
||||
overlay.remove();
|
||||
if (this.onRestart) this.onRestart();
|
||||
});
|
||||
|
||||
const certBtn = overlay.querySelector('#btn-aar-cert');
|
||||
if (certBtn) {
|
||||
certBtn.addEventListener('click', () => {
|
||||
overlay.remove();
|
||||
if (this.onClaimCertificate) this.onClaimCertificate();
|
||||
});
|
||||
}
|
||||
|
||||
const retryBtn = overlay.querySelector('#btn-aar-retry');
|
||||
if (retryBtn) {
|
||||
retryBtn.addEventListener('click', () => {
|
||||
overlay.remove();
|
||||
if (this.onRestart) this.onRestart();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* CyberSim OS - Multi-Dimensional Behavioral Scorer
|
||||
* Evaluates 7 core cybersecurity competency dimensions (0-100 scale, 80 passing threshold).
|
||||
*/
|
||||
|
||||
export class BehavioralScorer {
|
||||
constructor(scenario, eventLogs) {
|
||||
this.scenario = scenario;
|
||||
this.logs = eventLogs;
|
||||
this.passingThreshold = 80;
|
||||
}
|
||||
|
||||
evaluate() {
|
||||
const logs = this.logs;
|
||||
const feedback = [];
|
||||
const timeline = [];
|
||||
|
||||
// Helper checks
|
||||
const hasLog = (event, target = null) => logs.some(l => {
|
||||
if (target !== null) return l.event === event && l.target === target;
|
||||
return l.event === event;
|
||||
});
|
||||
|
||||
const findLogs = (event, target = null) => logs.filter(l => {
|
||||
if (target !== null) return l.event === event && l.target === target;
|
||||
return l.event === event;
|
||||
});
|
||||
|
||||
// 1. Threat Detection (20 pts max)
|
||||
let threatDetectionScore = 0;
|
||||
const openedPhish = hasLog('EMAIL_OPENED', 'email_phish_pwreset');
|
||||
const openedInvoice = hasLog('EMAIL_OPENED', 'email_malicious_invoice');
|
||||
|
||||
if (openedPhish) {
|
||||
threatDetectionScore += 10;
|
||||
timeline.push({ type: 'positive', text: 'Identified and opened the password reset notice for review.' });
|
||||
} else {
|
||||
feedback.push('You did not review the urgent password reset email during your shift.');
|
||||
}
|
||||
|
||||
if (openedInvoice) {
|
||||
threatDetectionScore += 10;
|
||||
timeline.push({ type: 'positive', text: 'Identified and reviewed the overdue vendor invoice notice.' });
|
||||
} else {
|
||||
feedback.push('You missed reviewing the overdue vendor invoice communication.');
|
||||
}
|
||||
|
||||
// 2. Investigation & Evidence Gathering (20 pts max)
|
||||
let investigationScore = 0;
|
||||
const inspectedPhishSender = hasLog('EMAIL_INSPECTED_SENDER', 'email_phish_pwreset');
|
||||
const hoveredPhishLink = hasLog('EMAIL_LINK_HOVERED', 'email_phish_pwreset');
|
||||
const visitedIntranetDir = hasLog('NAV_VISITED', 'http://intranet.nexacore.internal') || hasLog('NAV_VISITED', 'http://intranet.nexacore.internal/it-security');
|
||||
const checkedVendorPolicy = hasLog('FILE_OPENED', 'Vendor_Directory.pdf') || hasLog('DOC_VIEWED', 'Vendor_Directory.pdf');
|
||||
|
||||
if (inspectedPhishSender || hoveredPhishLink) {
|
||||
investigationScore += 10;
|
||||
timeline.push({ type: 'positive', text: 'Thoroughly investigated the phishing email headers and link destination.' });
|
||||
} else {
|
||||
feedback.push('You evaluated the password reset email without inspecting its sender header or hovering over the destination link.');
|
||||
}
|
||||
|
||||
if (visitedIntranetDir || checkedVendorPolicy) {
|
||||
investigationScore += 10;
|
||||
timeline.push({ type: 'positive', text: 'Consulted company resources (Intranet/Vendor Directory) to cross-reference ambiguous information.' });
|
||||
} else {
|
||||
feedback.push('Independent verification tools (Intranet directory, Approved Vendor Directory) were available but not consulted.');
|
||||
}
|
||||
|
||||
// 3. Safe Handling (15 pts max)
|
||||
let safeHandlingScore = 15;
|
||||
const enteredPhishCredentials = hasLog('NAV_FORM_SUBMITTED', 'phish_login_form');
|
||||
const downloadedMaliciousZip = hasLog('EMAIL_ATTACHMENT_OPENED', 'email_malicious_invoice') || hasLog('FILE_DOWNLOADED', 'Invoice_88921_Receipt.zip');
|
||||
|
||||
if (enteredPhishCredentials) {
|
||||
safeHandlingScore -= 10;
|
||||
timeline.push({ type: 'critical', text: 'Submitted enterprise credentials on an external credential harvesting portal.' });
|
||||
feedback.push('CRITICAL: You entered your password into an external lookalike portal (login-nexac0re-portal.com). Always check the browser address bar.');
|
||||
} else {
|
||||
timeline.push({ type: 'positive', text: 'Refrained from submitting credentials to untrusted external websites.' });
|
||||
}
|
||||
|
||||
if (downloadedMaliciousZip) {
|
||||
safeHandlingScore -= 5;
|
||||
timeline.push({ type: 'warning', text: 'Downloaded an unsolicited archive file from an unverified vendor.' });
|
||||
feedback.push('CAUTION: You opened an archive attachment from an unapproved vendor. Unsolicited archives often conceal malicious scripts or executables.');
|
||||
}
|
||||
|
||||
safeHandlingScore = Math.max(0, safeHandlingScore);
|
||||
|
||||
// 4. Independent Verification (15 pts max)
|
||||
let verificationScore = 0;
|
||||
const viewedPolicy = hasLog('DOC_VIEWED', 'NexaCore_Cyber_Security_Policy_v4.pdf') || hasLog('FILE_OPENED', 'NexaCore_Cyber_Security_Policy_v4.pdf');
|
||||
const checkedITSecurityPage = hasLog('NAV_VISITED', 'http://intranet.nexacore.internal/it-security');
|
||||
|
||||
if (viewedPolicy) {
|
||||
verificationScore += 8;
|
||||
timeline.push({ type: 'positive', text: 'Reviewed the NexaCore Cybersecurity Policy v4.' });
|
||||
}
|
||||
if (checkedITSecurityPage) {
|
||||
verificationScore += 7;
|
||||
timeline.push({ type: 'positive', text: 'Verified security announcements on the official IT Security Intranet portal.' });
|
||||
}
|
||||
|
||||
if (verificationScore === 0) {
|
||||
feedback.push('Take time to review official corporate policies and security bulletins before making decisions under pressure.');
|
||||
}
|
||||
|
||||
// 5. Incident Reporting (15 pts max)
|
||||
let reportingScore = 0;
|
||||
const reportedPhish = findLogs('EMAIL_REPORTED', 'email_phish_pwreset');
|
||||
const reportedInvoice = findLogs('EMAIL_REPORTED', 'email_malicious_invoice');
|
||||
|
||||
if (reportedPhish.length > 0) {
|
||||
reportingScore += 8;
|
||||
timeline.push({ type: 'positive', text: 'Promptly reported the SSO credential phishing threat to the Security Operations Center.' });
|
||||
} else {
|
||||
feedback.push('The phishing email was not reported to the Security Operations Center, leaving coworkers vulnerable.');
|
||||
}
|
||||
|
||||
if (reportedInvoice.length > 0) {
|
||||
reportingScore += 7;
|
||||
timeline.push({ type: 'positive', text: 'Reported the fake vendor invoice to the Security Center.' });
|
||||
}
|
||||
|
||||
// 6. False Positive Control (15 pts max)
|
||||
let falsePositiveScore = 15;
|
||||
const reportedLegitMFA = findLogs('EMAIL_REPORTED', 'email_legit_mfa');
|
||||
const reportedCoworker = findLogs('EMAIL_REPORTED', 'email_coworker_req');
|
||||
|
||||
if (reportedLegitMFA.length > 0) {
|
||||
falsePositiveScore -= 10;
|
||||
timeline.push({ type: 'warning', text: 'Incorrectly flagged the legitimate CISO MFA policy notice as phishing.' });
|
||||
feedback.push('NOTE: The MFA notice from Alex Rivera was legitimate. Verifying the sender on the employee directory and checking the intranet avoids false alarms.');
|
||||
}
|
||||
|
||||
if (reportedCoworker.length > 0) {
|
||||
falsePositiveScore -= 5;
|
||||
timeline.push({ type: 'warning', text: 'Flagged routine internal communication from Sarah Jenkins as suspicious.' });
|
||||
}
|
||||
|
||||
falsePositiveScore = Math.max(0, falsePositiveScore);
|
||||
|
||||
// Total Score Calculation (0-100)
|
||||
const totalScore = Math.min(100, Math.max(0,
|
||||
threatDetectionScore +
|
||||
investigationScore +
|
||||
safeHandlingScore +
|
||||
verificationScore +
|
||||
reportingScore +
|
||||
falsePositiveScore
|
||||
));
|
||||
|
||||
const isPassed = totalScore >= this.passingThreshold;
|
||||
|
||||
return {
|
||||
totalScore,
|
||||
isPassed,
|
||||
passingThreshold: this.passingThreshold,
|
||||
categories: {
|
||||
threatDetection: { score: threatDetectionScore, max: 20, label: 'Threat Detection' },
|
||||
investigation: { score: investigationScore, max: 20, label: 'Investigation & Evidence' },
|
||||
safeHandling: { score: safeHandlingScore, max: 15, label: 'Safe Handling' },
|
||||
verification: { score: verificationScore, max: 15, label: 'Independent Verification' },
|
||||
reporting: { score: reportingScore, max: 15, label: 'Incident Reporting' },
|
||||
falsePositiveControl: { score: falsePositiveScore, max: 15, label: 'False Positive Control' }
|
||||
},
|
||||
timeline,
|
||||
feedback: feedback.length > 0 ? feedback : ['Outstanding performance! You investigated all anomalies with sound judgment and protected company assets.']
|
||||
};
|
||||
}
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>CyberSim OS - Standalone Certificate Verifier</title>
|
||||
<link rel="stylesheet" href="css/theme-windows.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<style>
|
||||
body {
|
||||
background: #0f172a;
|
||||
color: #f8fafc;
|
||||
font-family: var(--font-system);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
.verify-card {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 30px;
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
box-shadow: 0 20px 48px rgba(0,0,0,0.5);
|
||||
}
|
||||
.dropzone {
|
||||
border: 2px dashed #475569;
|
||||
border-radius: var(--radius-md);
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
margin: 20px 0;
|
||||
transition: all 0.15s ease;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
}
|
||||
.dropzone:hover, .dropzone.dragover {
|
||||
border-color: #3b82f6;
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
}
|
||||
.result-box {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.result-box.valid {
|
||||
background: #064e3b;
|
||||
border: 1px solid #059669;
|
||||
color: #d1fae5;
|
||||
}
|
||||
.result-box.invalid {
|
||||
background: #7f1d1d;
|
||||
border: 1px solid #dc2626;
|
||||
color: #fee2e2;
|
||||
}
|
||||
.cert-field-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
margin-top: 12px;
|
||||
font-family: var(--font-mono);
|
||||
background: rgba(0,0,0,0.2);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="verify-card">
|
||||
<div style="display:flex; align-items:center; gap:10px; margin-bottom:6px;">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/></svg>
|
||||
<h1 style="font-size:18px; font-weight:700; color:#f8fafc;">CyberSim Certificate Verifier</h1>
|
||||
</div>
|
||||
<p style="font-size:12px; color:#94a3b8;">Offline-first cryptographic credential integrity and scoring verification engine.</p>
|
||||
|
||||
<div class="dropzone" id="dropzone">
|
||||
<div style="font-size:32px; margin-bottom:8px;">📄</div>
|
||||
<div style="font-weight:600; font-size:13px;">Drag & Drop a *.cybercert credential file here</div>
|
||||
<div style="font-size:11px; color:#94a3b8; margin-top:4px;">or click to browse local files</div>
|
||||
<input type="file" id="file-input" accept=".cybercert,.json" style="display:none;">
|
||||
</div>
|
||||
|
||||
<div id="result-container" style="display:none;"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import { CertificateVerifier } from './js/cert/cert_verifier.js';
|
||||
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
const fileInput = document.getElementById('file-input');
|
||||
const resultContainer = document.getElementById('result-container');
|
||||
|
||||
dropzone.addEventListener('click', () => fileInput.click());
|
||||
|
||||
dropzone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.add('dragover');
|
||||
});
|
||||
|
||||
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dragover'));
|
||||
|
||||
dropzone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('dragover');
|
||||
if (e.dataTransfer.files.length > 0) {
|
||||
processFile(e.dataTransfer.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
processFile(e.target.files[0]);
|
||||
}
|
||||
});
|
||||
|
||||
async function processFile(file) {
|
||||
try {
|
||||
const text = await file.text();
|
||||
const json = JSON.parse(text);
|
||||
const res = await CertificateVerifier.verify(json);
|
||||
|
||||
resultContainer.style.display = 'block';
|
||||
|
||||
if (res.valid) {
|
||||
const d = res.certData;
|
||||
resultContainer.className = 'result-box valid';
|
||||
resultContainer.innerHTML = `
|
||||
<div style="font-weight:700; font-size:14px; margin-bottom:4px;">✓ Certificate Cryptographically Verified</div>
|
||||
<div style="font-size:12px;">This credential was verified offline using SHA-256 integrity validation.</div>
|
||||
|
||||
<div class="cert-field-grid">
|
||||
<div><strong>Learner:</strong> ${d.learner.name}</div>
|
||||
<div><strong>Role:</strong> ${d.learner.assigned_role}</div>
|
||||
<div><strong>Score:</strong> ${d.evaluation.score} / ${d.evaluation.max_score} (${d.evaluation.passed ? 'PASSED' : 'FAILED'})</div>
|
||||
<div><strong>Date:</strong> ${new Date(d.issued_at).toLocaleDateString()}</div>
|
||||
<div><strong>Scenario:</strong> ${d.scenario_title}</div>
|
||||
<div><strong>Certificate ID:</strong> ${d.certificate_id.substring(0, 16)}...</div>
|
||||
<div style="grid-column: 1/-1; word-break:break-all;"><strong>Scenario SHA-256:</strong> ${d.scenario_fingerprint}</div>
|
||||
<div style="grid-column: 1/-1; word-break:break-all;"><strong>Integrity Hash:</strong> ${d.integrity_hash}</div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
resultContainer.className = 'result-box invalid';
|
||||
resultContainer.innerHTML = `
|
||||
<div style="font-weight:700; font-size:14px; margin-bottom:4px;">✕ Verification Failed</div>
|
||||
<div>${res.error || 'Integrity check failed.'}</div>
|
||||
`;
|
||||
}
|
||||
} catch (err) {
|
||||
resultContainer.style.display = 'block';
|
||||
resultContainer.className = 'result-box invalid';
|
||||
resultContainer.innerHTML = `<div style="font-weight:700;">✕ Invalid File Format</div><div>The file could not be parsed as a valid CyberSim credential: ${err.message}</div>`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user