From 36430b1e3004e0318cf7131d98ce01bed2b60d65 Mon Sep 17 00:00:00 2001 From: YukiCodepth Date: Fri, 24 Apr 2026 12:31:55 +0530 Subject: [PATCH 1/5] fix: return empty favicon response --- backend/app.py | 6 +++--- tests/test_api_backend.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/app.py b/backend/app.py index 9968ea2..a8c83a4 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4,7 +4,7 @@ from pathlib import Path from fastapi import FastAPI, File, Form, HTTPException, UploadFile -from fastapi.responses import FileResponse, JSONResponse +from fastapi.responses import FileResponse, Response from fastapi.staticfiles import StaticFiles from cli.diff_engine import diff_scan_results @@ -113,8 +113,8 @@ def root() -> dict[str, object]: @app.get("/favicon.ico") -def favicon() -> JSONResponse: - return JSONResponse(status_code=204, content=None) +def favicon() -> Response: + return Response(status_code=204) @app.get("/dashboard") diff --git a/tests/test_api_backend.py b/tests/test_api_backend.py index af5d7fc..ecd10db 100644 --- a/tests/test_api_backend.py +++ b/tests/test_api_backend.py @@ -33,6 +33,11 @@ def test_root_endpoint(self) -> None: self.assertEqual(payload["docs_url"], "/docs") self.assertEqual(payload["api_base"], "/api/v1") + def test_favicon_no_content(self) -> None: + response = self.client.get("/favicon.ico") + self.assertEqual(response.status_code, 204) + self.assertEqual(response.content, b"") + def test_dashboard_entrypoint(self) -> None: response = self.client.get("/dashboard") self.assertEqual(response.status_code, 200) From 1a4a2f365ae6bf65a59a5aedc49ce6eb1f842cda Mon Sep 17 00:00:00 2001 From: YukiCodepth Date: Fri, 24 Apr 2026 12:36:43 +0530 Subject: [PATCH 2/5] Phase 18: add desktop app shell scaffold --- README.md | 19 +- ROADMAP.md | 6 +- desktop/app/index.html | 138 ++++++++++ desktop/app/main.js | 166 ++++++++++++ desktop/app/styles.css | 397 +++++++++++++++++++++++++++++ desktop/package.json | 14 + desktop/src-tauri/Cargo.toml | 18 ++ desktop/src-tauri/build.rs | 3 + desktop/src-tauri/src/lib.rs | 7 + desktop/src-tauri/src/main.rs | 3 + desktop/src-tauri/tauri.conf.json | 35 +++ docs/phase-18-desktop-app-shell.md | 56 ++++ tests/test_desktop_shell.py | 48 ++++ 13 files changed, 906 insertions(+), 4 deletions(-) create mode 100644 desktop/app/index.html create mode 100644 desktop/app/main.js create mode 100644 desktop/app/styles.css create mode 100644 desktop/package.json create mode 100644 desktop/src-tauri/Cargo.toml create mode 100644 desktop/src-tauri/build.rs create mode 100644 desktop/src-tauri/src/lib.rs create mode 100644 desktop/src-tauri/src/main.rs create mode 100644 desktop/src-tauri/tauri.conf.json create mode 100644 docs/phase-18-desktop-app-shell.md create mode 100644 tests/test_desktop_shell.py diff --git a/README.md b/README.md index f0d59af..929f450 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Firmware security tools are often either very advanced research tools or small c - Firmware version diffing - Firmware Risk DNA profile - Hardening Simulator with what-if security action planning +- Desktop app shell foundation for macOS, Windows, and Linux - HTML, Markdown, and JSON reports - Sample vulnerable firmware corpus for demos @@ -62,9 +63,9 @@ git checkout -b phase/02-cli-scanner-mvp ## Current Status -Current status: `Phase 17 - Hardening Simulator` +Current status: `Phase 18 - Desktop App Shell` -The project includes the full roadmap feature set through `v1.0.0` and an innovation extension: Hardening Simulator for prioritized remediation actions and projected risk reduction scenarios. +The project includes the full roadmap feature set through `v1.0.0`, the Phase 17 Hardening Simulator innovation, and a Phase 18 desktop app shell foundation for future native installers. ## Quick Start @@ -171,6 +172,19 @@ Dashboard: http://127.0.0.1:8000/dashboard ``` +Preview desktop shell: + +```bash +cd desktop +python3 -m http.server 4173 --directory app +``` + +Then open: + +```text +http://127.0.0.1:4173 +``` + ## Safety Scope This project is for defensive firmware analysis, developer education, and security auditing. It does not include exploit generation, unauthorized device access, credential abuse, or malware deployment. @@ -182,6 +196,7 @@ backend/ FastAPI backend cli/ Command-line scanner docs/ Project docs, architecture, learning notes frontend/ Dashboard UI +desktop/ Desktop app shell and Tauri scaffold .github/ CI workflow Dockerfile Container packaging reports/ Report templates and generated report output diff --git a/ROADMAP.md b/ROADMAP.md index e2aad67..495220c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -21,6 +21,7 @@ We will not create a GitHub Release after every phase. Releases happen only at m | `v0.9.0` | Reports + Packaging RC | Reports, Docker, CI, and release candidate polish | After Phase 15 | | `v1.0.0` | Stable Open-Source Release | Complete polished release | After Phase 16 | | `v1.1.0` | Hardening Simulator Innovation | What-if remediation planning with projected risk reduction | After Phase 17 | +| `v1.2.0` | Desktop App Alpha | Native shell foundation for macOS, Windows, and Linux | After Phase 18 | Packages are not needed yet. Later we may publish a Docker image, a Python CLI package, and standalone binaries if the project is stable enough. @@ -47,6 +48,7 @@ Packages are not needed yet. Later we may publish a Docker image, a Python CLI p | 15 | `phase/15-packaging-ci` | Package and automate | Docker and GitHub Actions | `v0.9.0` | | 16 | `phase/16-final-showcase` | Polish release | Screenshots, demo, docs, `v1.0.0` tag | `v1.0.0` | | 17 | `phase/17-hardening-simulator` | Add unique hardening simulation | Prioritized mitigation actions + what-if scenarios | `v1.1.0` | +| 18 | `phase/18-desktop-app-shell` | Start desktop app | Tauri-ready shell and polished desktop UI | `v1.2.0` alpha | ## Build Philosophy @@ -113,6 +115,6 @@ A phase is complete only when: ## Current Build Focus -Current roadmap phase: `Phase 17 - Hardening Simulator` +Current roadmap phase: `Phase 18 - Desktop App Shell` -Next implementation phase: `Release prep for v1.1.0` +Next implementation phase: `Phase 18 - Desktop App Shell` diff --git a/desktop/app/index.html b/desktop/app/index.html new file mode 100644 index 0000000..54294a9 --- /dev/null +++ b/desktop/app/index.html @@ -0,0 +1,138 @@ + + + + + + Firmware Security Workbench Desktop + + + +
+ + +
+
+
+

Firmware Security Workbench

+

Mission Control

+
+
+ API offline + +
+
+ +
+
+
+ 69 + high +
+
+

Risk DNA

+ 48f1b3013024db3635cd2440 +

CREDS / NET / RULES / FINDINGS

+
+
+ +
+
+ Findings + 5 +
+
+ Secrets + 1 +
+
+ CVE candidates + 0 +
+
+ Hardening actions + 7 +
+
+
+ +
+
+

Scan Studio

+ +
+
+ +
+ + + + +
+
+
+ +
+
+
+

Evidence Feed

+ demo-firmware.bin +
+
    +
    + +
    +
    +

    Hardening Studio

    + Projected 20 +
    +
    +
      +
      +
      + +
      +
      +

      Release Timeline

      + v1.1 desktop foundation +
      +
      +
      CLI
      +
      API
      +
      Dashboard
      +
      Risk DNA
      +
      Desktop
      +
      Installers
      +
      +
      +
      +
      + + + + diff --git a/desktop/app/main.js b/desktop/app/main.js new file mode 100644 index 0000000..b45fa42 --- /dev/null +++ b/desktop/app/main.js @@ -0,0 +1,166 @@ +const demoScan = { + file: { name: "demo-firmware.bin" }, + analysis: { + suspicious_count: 5, + secret_exposure_count: 1, + cve_candidate_count: 0, + suspicious_findings: [ + { severity: "high", offset_hex: "0x56", string: "wifi_password=demo1234" }, + { severity: "medium", offset_hex: "0x6d", string: "mqtt://broker.internal.local:1883" }, + { severity: "medium", offset_hex: "0x8f", string: "ota_update_url=http://updates.internal.local/fw.bin" }, + { severity: "medium", offset_hex: "0xd8", string: "admin_panel_enabled=true" }, + { severity: "low", offset_hex: "0xc3", string: "DEBUG: boot complete" } + ], + risk_dna: { + score: 69, + band: "high", + fingerprint: "48f1b3013024db3635cd2440", + tags: ["CREDS", "NET", "RULES", "FINDINGS"] + }, + hardening_simulation: { + projected: { score: 20, band: "low", estimated_reduction: 49 }, + actions_count: 7, + scenarios: [ + { name: "quick-patch", projected_score: 57, projected_band: "high", reduction: 12 }, + { name: "balanced-sprint", projected_score: 29, projected_band: "low", reduction: 40 }, + { name: "aggressive-lockdown", projected_score: 14, projected_band: "low", reduction: 55 } + ], + actions: [ + { title: "Rotate embedded credentials and move secrets to secure storage", effort: "medium", estimated_risk_reduction: 18 }, + { title: "Enforce TLS and authenticated transport for firmware network paths", effort: "medium", estimated_risk_reduction: 14 }, + { title: "Enforce signed OTA manifests and anti-rollback controls", effort: "high", estimated_risk_reduction: 12 }, + { title: "Harden admin interfaces with least privilege and explicit authz", effort: "medium", estimated_risk_reduction: 9 } + ] + } + } +}; + +const refs = { + apiState: document.getElementById("api-state"), + refreshBtn: document.getElementById("refresh-btn"), + demoBtn: document.getElementById("demo-btn"), + scanForm: document.getElementById("scan-form"), + firmwareFile: document.getElementById("firmware-file"), + fileLabel: document.getElementById("file-label"), + minStringLength: document.getElementById("min-string-length"), + maxStrings: document.getElementById("max-strings"), + saveScan: document.getElementById("save-scan"), + selectedFile: document.getElementById("selected-file"), + riskScore: document.getElementById("risk-score"), + riskBand: document.getElementById("risk-band"), + riskFingerprint: document.getElementById("risk-fingerprint"), + riskTags: document.getElementById("risk-tags"), + metricFindings: document.getElementById("metric-findings"), + metricSecrets: document.getElementById("metric-secrets"), + metricCves: document.getElementById("metric-cves"), + metricActions: document.getElementById("metric-actions"), + projectedScore: document.getElementById("projected-score"), + findingList: document.getElementById("finding-list"), + scenarioList: document.getElementById("scenario-list"), + actionList: document.getElementById("action-list") +}; + +function getApiBase() { + return "http://127.0.0.1:8000"; +} + +function setApiState(online) { + refs.apiState.textContent = online ? "API online" : "API offline"; + refs.apiState.classList.toggle("good", online); +} + +function safeArray(value) { + return Array.isArray(value) ? value : []; +} + +function renderScan(scan) { + const file = scan.file || {}; + const analysis = scan.analysis || {}; + const risk = analysis.risk_dna || {}; + const hardening = analysis.hardening_simulation || {}; + const projected = hardening.projected || {}; + + refs.selectedFile.textContent = file.name || "unsaved scan"; + refs.riskScore.textContent = risk.score ?? "-"; + refs.riskBand.textContent = risk.band || "-"; + refs.riskFingerprint.textContent = risk.fingerprint || "-"; + refs.riskTags.textContent = safeArray(risk.tags).join(" / ") || "BASELINE"; + refs.metricFindings.textContent = analysis.suspicious_count ?? 0; + refs.metricSecrets.textContent = analysis.secret_exposure_count ?? 0; + refs.metricCves.textContent = analysis.cve_candidate_count ?? 0; + refs.metricActions.textContent = hardening.actions_count ?? 0; + refs.projectedScore.textContent = `Projected ${projected.score ?? "-"}`; + + refs.findingList.innerHTML = ""; + for (const finding of safeArray(analysis.suspicious_findings).slice(0, 8)) { + const li = document.createElement("li"); + li.innerHTML = `${finding.severity || "info"}${finding.offset_hex || "-"} ${finding.string || ""}`; + refs.findingList.appendChild(li); + } + + refs.scenarioList.innerHTML = ""; + for (const scenario of safeArray(hardening.scenarios).slice(0, 3)) { + const card = document.createElement("div"); + card.className = "scenario-card"; + card.innerHTML = `${scenario.name}score ${scenario.projected_score} (${scenario.projected_band}), reduction ${scenario.reduction}`; + refs.scenarioList.appendChild(card); + } + + refs.actionList.innerHTML = ""; + for (const action of safeArray(hardening.actions).slice(0, 6)) { + const li = document.createElement("li"); + li.innerHTML = `${action.title || "Hardening action"}
      effort ${action.effort || "-"} / reduction ${action.estimated_risk_reduction || 0}
      `; + refs.actionList.appendChild(li); + } +} + +async function checkApi() { + try { + const response = await fetch(`${getApiBase()}/health`); + setApiState(response.ok); + } catch { + setApiState(false); + } +} + +async function submitScan(event) { + event.preventDefault(); + const file = refs.firmwareFile.files?.[0]; + if (!file) { + renderScan(demoScan); + return; + } + + const formData = new FormData(); + formData.append("file", file); + formData.append("min_string_length", refs.minStringLength.value || "4"); + formData.append("max_strings", refs.maxStrings.value || "2000"); + formData.append("save", refs.saveScan.checked ? "true" : "false"); + + try { + const response = await fetch(`${getApiBase()}/api/v1/scans`, { + method: "POST", + body: formData + }); + if (!response.ok) { + throw new Error(await response.text()); + } + renderScan(await response.json()); + setApiState(true); + } catch { + setApiState(false); + renderScan(demoScan); + } +} + +refs.firmwareFile.addEventListener("change", () => { + const file = refs.firmwareFile.files?.[0]; + refs.fileLabel.textContent = file ? file.name : "Drop firmware or choose a file"; +}); + +refs.scanForm.addEventListener("submit", submitScan); +refs.demoBtn.addEventListener("click", () => renderScan(demoScan)); +refs.refreshBtn.addEventListener("click", checkApi); + +renderScan(demoScan); +checkApi(); diff --git a/desktop/app/styles.css b/desktop/app/styles.css new file mode 100644 index 0000000..84eaf6a --- /dev/null +++ b/desktop/app/styles.css @@ -0,0 +1,397 @@ +:root { + color-scheme: dark; + --bg: #151515; + --panel: #20211f; + --panel-strong: #292a26; + --line: #3a3d35; + --text: #f3f0e8; + --muted: #b5b0a4; + --moss: #8db67b; + --brass: #d2a64c; + --ember: #e0673f; + --aqua: #76c7c0; + --danger: #e95f5a; + --shadow: 0 24px 80px rgba(0, 0, 0, 0.28); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-width: 1024px; + min-height: 100vh; + background: + linear-gradient(135deg, rgba(141, 182, 123, 0.14), transparent 36%), + radial-gradient(circle at 80% 8%, rgba(210, 166, 76, 0.16), transparent 30%), + var(--bg); + color: var(--text); + font-family: Avenir Next, Avenir, Helvetica Neue, sans-serif; + letter-spacing: 0; +} + +button, +input { + font: inherit; +} + +.app-shell { + display: grid; + grid-template-columns: 76px minmax(0, 1fr); + min-height: 100vh; +} + +.rail { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + padding: 18px 12px; + border-right: 1px solid var(--line); + background: rgba(21, 21, 21, 0.86); + backdrop-filter: blur(20px); +} + +.brand-mark { + display: grid; + place-items: center; + width: 48px; + height: 48px; + margin-bottom: 12px; + border: 1px solid rgba(141, 182, 123, 0.55); + border-radius: 8px; + background: linear-gradient(145deg, rgba(141, 182, 123, 0.2), rgba(118, 199, 192, 0.08)); + color: var(--moss); + font-weight: 800; +} + +.rail-button, +.icon-button { + display: grid; + place-items: center; + width: 44px; + height: 44px; + border: 1px solid transparent; + border-radius: 8px; + background: transparent; + color: var(--muted); + cursor: pointer; +} + +.rail-button:hover, +.icon-button:hover, +.rail-button.active { + border-color: var(--line); + background: var(--panel-strong); + color: var(--text); +} + +.workspace { + display: grid; + gap: 18px; + padding: 24px; +} + +.topbar, +.panel-title-row, +.topbar-actions, +.scan-controls, +.metric-strip, +.risk-orbit { + display: flex; + align-items: center; +} + +.topbar { + justify-content: space-between; +} + +.eyebrow, +.metric-label { + display: block; + margin: 0 0 6px; + color: var(--muted); + font-size: 12px; + font-weight: 700; + text-transform: uppercase; +} + +h1, +h2 { + margin: 0; + letter-spacing: 0; +} + +h1 { + font-size: 34px; +} + +h2 { + font-size: 18px; +} + +.topbar-actions { + gap: 10px; +} + +.status-pill { + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 0 12px; + border: 1px solid var(--line); + border-radius: 8px; + color: var(--brass); + background: rgba(210, 166, 76, 0.08); + font-size: 13px; + font-weight: 700; +} + +.status-pill.good { + color: var(--moss); + background: rgba(141, 182, 123, 0.1); +} + +.hero-grid { + display: grid; + grid-template-columns: 1.15fr 1fr; + gap: 18px; +} + +.risk-orbit, +.metric-strip, +.command-surface, +.panel { + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(32, 33, 31, 0.9); + box-shadow: var(--shadow); +} + +.risk-orbit { + min-height: 164px; + gap: 22px; + padding: 22px; +} + +.score-ring { + display: grid; + place-items: center; + width: 116px; + height: 116px; + border-radius: 50%; + border: 10px solid rgba(224, 103, 63, 0.34); + background: linear-gradient(145deg, rgba(224, 103, 63, 0.18), rgba(210, 166, 76, 0.08)); +} + +.score-ring span { + font-size: 38px; + font-weight: 800; +} + +.score-ring small { + color: var(--muted); + font-weight: 800; + text-transform: uppercase; +} + +.metric-strip { + justify-content: space-between; + padding: 22px; +} + +.metric-strip strong { + display: block; + font-size: 32px; +} + +.muted { + color: var(--muted); +} + +.command-surface, +.panel { + padding: 18px; +} + +.panel-title-row { + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.scan-form { + display: grid; + grid-template-columns: minmax(280px, 1fr) 1.2fr; + gap: 16px; +} + +.file-drop { + display: grid; + place-items: center; + min-height: 112px; + border: 1px dashed rgba(118, 199, 192, 0.58); + border-radius: 8px; + background: rgba(118, 199, 192, 0.08); + color: var(--aqua); + font-weight: 800; + cursor: pointer; +} + +.file-drop input { + display: none; +} + +.scan-controls { + gap: 12px; + flex-wrap: wrap; +} + +.scan-controls label { + display: grid; + gap: 6px; + min-width: 150px; +} + +.scan-controls label span, +.switch span { + color: var(--muted); + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.scan-controls input[type="number"] { + width: 100%; + height: 42px; + border: 1px solid var(--line); + border-radius: 8px; + background: #171816; + color: var(--text); + padding: 0 12px; +} + +.switch { + display: flex !important; + align-items: center; + min-width: 120px !important; +} + +.primary-button, +.ghost-button { + height: 42px; + border: 1px solid transparent; + border-radius: 8px; + padding: 0 18px; + font-weight: 800; + cursor: pointer; +} + +.primary-button { + background: var(--moss); + color: #11140f; +} + +.ghost-button { + border-color: var(--line); + background: transparent; + color: var(--text); +} + +.content-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; +} + +.evidence-list, +.action-list { + display: grid; + gap: 10px; + list-style: none; + margin: 0; + padding: 0; +} + +.evidence-list li, +.action-list li, +.scenario-card { + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(21, 21, 21, 0.46); + padding: 12px; +} + +.severity { + display: inline-block; + min-width: 68px; + margin-right: 8px; + color: var(--ember); + font-size: 12px; + font-weight: 800; + text-transform: uppercase; +} + +.scenario-list { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 14px; +} + +.scenario-card strong { + display: block; + margin-bottom: 6px; +} + +.timeline { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 10px; +} + +.timeline-item { + border: 1px solid var(--line); + border-radius: 8px; + padding: 12px; + color: var(--muted); + text-align: center; + font-weight: 800; +} + +.timeline-item.done { + border-color: rgba(141, 182, 123, 0.6); + color: var(--moss); +} + +.timeline-item.active { + border-color: rgba(210, 166, 76, 0.76); + color: var(--brass); +} + +@media (max-width: 1100px) { + body { + min-width: 0; + } + + .app-shell, + .hero-grid, + .content-grid, + .scan-form { + grid-template-columns: 1fr; + } + + .rail { + display: none; + } + + .workspace { + padding: 16px; + } + + .scenario-list, + .timeline { + grid-template-columns: 1fr; + } +} diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..70a499a --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,14 @@ +{ + "name": "firmware-security-workbench-desktop", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tauri dev", + "build": "tauri build", + "preview": "python3 -m http.server 4173 --directory app" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0" + } +} diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..86fb051 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "firmware-security-workbench" +version = "0.1.0" +description = "Firmware Security Workbench desktop shell" +authors = ["Firmware Security Workbench Contributors"] +edition = "2021" +rust-version = "1.77" + +[lib] +name = "firmware_security_workbench_desktop_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = [] } +tauri-plugin-opener = "2" diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs new file mode 100644 index 0000000..39235d6 --- /dev/null +++ b/desktop/src-tauri/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + tauri::Builder::default() + .plugin(tauri_plugin_opener::init()) + .run(tauri::generate_context!()) + .expect("error while running Firmware Security Workbench desktop"); +} diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..6c4a51b --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + firmware_security_workbench_desktop_lib::run(); +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..44790d5 --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Firmware Security Workbench", + "version": "0.1.0", + "identifier": "dev.fwb.desktop", + "build": { + "beforeDevCommand": "npm run preview", + "beforeBuildCommand": "", + "devUrl": "http://127.0.0.1:4173", + "frontendDist": "../app" + }, + "app": { + "windows": [ + { + "title": "Firmware Security Workbench", + "width": 1280, + "height": 860, + "minWidth": 1024, + "minHeight": 720, + "resizable": true, + "fullscreen": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "category": "DeveloperTool", + "shortDescription": "Firmware analysis desktop workbench", + "longDescription": "A local-first firmware security analysis desktop shell for scanning firmware, reviewing Risk DNA, and planning hardening actions." + } +} diff --git a/docs/phase-18-desktop-app-shell.md b/docs/phase-18-desktop-app-shell.md new file mode 100644 index 0000000..7860e2d --- /dev/null +++ b/docs/phase-18-desktop-app-shell.md @@ -0,0 +1,56 @@ +# Phase 18: Desktop App Shell + +## Goal + +Phase 18 starts the move from browser dashboard to native desktop app for macOS, Windows, and Linux. + +The selected direction is Tauri 2 with a local web frontend. This keeps the app lightweight and gives us a future path for bundling the Python scanner as a sidecar binary. + +## What This Phase Adds + +- `desktop/app`: polished desktop-first UI shell that can run in a browser today. +- `desktop/src-tauri`: native Tauri 2 scaffold for later macOS, Windows, and Linux installers. +- API-aware scan form that calls the existing FastAPI backend when it is running. +- Demo fallback data so the desktop shell still looks alive when the API is offline. +- Risk DNA, evidence feed, hardening scenarios, and release timeline views. + +## How To Preview Today + +Start the existing backend: + +```bash +uvicorn backend.app:app --reload --port 8000 +``` + +In a second terminal: + +```bash +cd desktop +python3 -m http.server 4173 --directory app +``` + +Open: + +```text +http://127.0.0.1:4173 +``` + +## How Native Packaging Will Work Later + +The desktop app will eventually bundle the scanner using a Tauri sidecar. Tauri requires platform-specific sidecar binaries with target triples, so we will add those in a later phase after the UI shell is stable. + +Planned packaging path: + +- macOS: `.app` and `.dmg` +- Windows: `.msi` or NSIS installer +- Linux: `.deb`, `.rpm`, or AppImage + +## Next Phase + +Phase 19 should turn the shell into a real desktop workflow: + +- native file picker +- scan queue +- local result store +- desktop report viewer +- backend launcher or bundled scanner sidecar diff --git a/tests/test_desktop_shell.py b/tests/test_desktop_shell.py new file mode 100644 index 0000000..1c11f45 --- /dev/null +++ b/tests/test_desktop_shell.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import json +import unittest +from pathlib import Path + + +class DesktopShellTests(unittest.TestCase): + def setUp(self) -> None: + self.repo_root = Path(__file__).resolve().parents[1] + + def test_desktop_shell_files_exist(self) -> None: + expected_files = [ + "desktop/app/index.html", + "desktop/app/styles.css", + "desktop/app/main.js", + "desktop/package.json", + "desktop/src-tauri/tauri.conf.json", + "desktop/src-tauri/Cargo.toml", + "desktop/src-tauri/src/main.rs", + "docs/phase-18-desktop-app-shell.md", + ] + for relative_path in expected_files: + with self.subTest(path=relative_path): + self.assertTrue((self.repo_root / relative_path).exists()) + + def test_desktop_shell_contains_core_views(self) -> None: + html = (self.repo_root / "desktop" / "app" / "index.html").read_text( + encoding="utf-8" + ) + self.assertIn("Mission Control", html) + self.assertIn("Scan Studio", html) + self.assertIn("Hardening Studio", html) + self.assertIn("Release Timeline", html) + + def test_tauri_config_points_to_desktop_app(self) -> None: + config = json.loads( + (self.repo_root / "desktop" / "src-tauri" / "tauri.conf.json").read_text( + encoding="utf-8" + ) + ) + self.assertEqual(config["productName"], "Firmware Security Workbench") + self.assertEqual(config["build"]["frontendDist"], "../app") + self.assertEqual(config["build"]["devUrl"], "http://127.0.0.1:4173") + + +if __name__ == "__main__": + unittest.main() From b18c031b74a13afac0a3b5c281dd10dc86b220a4 Mon Sep 17 00:00:00 2001 From: YukiCodepth Date: Fri, 24 Apr 2026 12:46:34 +0530 Subject: [PATCH 3/5] Phase 19: redesign UI and add desktop packaging workflow --- .github/workflows/desktop-packages.yml | 56 ++ .gitignore | 1 + README.md | 10 +- ROADMAP.md | 6 +- desktop/app/styles.css | 155 +++--- desktop/src-tauri/capabilities/default.json | 7 + docs/phase-19-nextgen-ui-packaging.md | 75 +++ frontend/dashboard.css | 565 ++++++++++++-------- frontend/dashboard.js | 12 + frontend/index.html | 358 +++++++------ tests/test_desktop_shell.py | 12 + 11 files changed, 817 insertions(+), 440 deletions(-) create mode 100644 .github/workflows/desktop-packages.yml create mode 100644 desktop/src-tauri/capabilities/default.json create mode 100644 docs/phase-19-nextgen-ui-packaging.md diff --git a/.github/workflows/desktop-packages.yml b/.github/workflows/desktop-packages.yml new file mode 100644 index 0000000..15e4d6c --- /dev/null +++ b/.github/workflows/desktop-packages.yml @@ -0,0 +1,56 @@ +name: Desktop Packages + +on: + workflow_dispatch: + push: + tags: + - "desktop-v*" + +jobs: + build-desktop: + name: Build ${{ matrix.platform }} + runs-on: ${{ matrix.platform }} + strategy: + fail-fast: false + matrix: + platform: + - macos-latest + - windows-latest + - ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Linux desktop dependencies + if: matrix.platform == 'ubuntu-latest' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Install desktop dependencies + working-directory: desktop + run: npm install + + - name: Build desktop package + working-directory: desktop + run: npm run build + + - name: Upload desktop bundle + uses: actions/upload-artifact@v4 + with: + name: fwb-desktop-${{ matrix.platform }} + path: desktop/src-tauri/target/release/bundle/**/* + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 1c1d3c8..4f90ea8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ venv/ node_modules/ dist/ build/ +desktop/src-tauri/target/ # Local output reports/generated/ diff --git a/README.md b/README.md index 929f450..8da3215 100644 --- a/README.md +++ b/README.md @@ -63,9 +63,9 @@ git checkout -b phase/02-cli-scanner-mvp ## Current Status -Current status: `Phase 18 - Desktop App Shell` +Current status: `Phase 19 - Next-Gen UI + Desktop Packaging` -The project includes the full roadmap feature set through `v1.0.0`, the Phase 17 Hardening Simulator innovation, and a Phase 18 desktop app shell foundation for future native installers. +The project includes the full roadmap feature set through `v1.0.0`, the Phase 17 Hardening Simulator innovation, a desktop app shell, a redesigned next-gen dashboard, and a GitHub Actions workflow for macOS, Windows, and Linux desktop package artifacts. ## Quick Start @@ -185,6 +185,12 @@ Then open: http://127.0.0.1:4173 ``` +Build desktop packages on GitHub: + +```text +Actions -> Desktop Packages -> Run workflow +``` + ## Safety Scope This project is for defensive firmware analysis, developer education, and security auditing. It does not include exploit generation, unauthorized device access, credential abuse, or malware deployment. diff --git a/ROADMAP.md b/ROADMAP.md index 495220c..9aad8be 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -22,6 +22,7 @@ We will not create a GitHub Release after every phase. Releases happen only at m | `v1.0.0` | Stable Open-Source Release | Complete polished release | After Phase 16 | | `v1.1.0` | Hardening Simulator Innovation | What-if remediation planning with projected risk reduction | After Phase 17 | | `v1.2.0` | Desktop App Alpha | Native shell foundation for macOS, Windows, and Linux | After Phase 18 | +| `v1.3.0` | Next-Gen Desktop Package Preview | Redesigned UI and first desktop package workflow | After Phase 19 | Packages are not needed yet. Later we may publish a Docker image, a Python CLI package, and standalone binaries if the project is stable enough. @@ -49,6 +50,7 @@ Packages are not needed yet. Later we may publish a Docker image, a Python CLI p | 16 | `phase/16-final-showcase` | Polish release | Screenshots, demo, docs, `v1.0.0` tag | `v1.0.0` | | 17 | `phase/17-hardening-simulator` | Add unique hardening simulation | Prioritized mitigation actions + what-if scenarios | `v1.1.0` | | 18 | `phase/18-desktop-app-shell` | Start desktop app | Tauri-ready shell and polished desktop UI | `v1.2.0` alpha | +| 19 | `phase/19-nextgen-ui-packaging` | Upgrade UI and package workflow | Next-gen dashboard, desktop shell polish, cross-OS GitHub packaging | `v1.3.0` preview | ## Build Philosophy @@ -115,6 +117,6 @@ A phase is complete only when: ## Current Build Focus -Current roadmap phase: `Phase 18 - Desktop App Shell` +Current roadmap phase: `Phase 19 - Next-Gen UI + Desktop Packaging` -Next implementation phase: `Phase 18 - Desktop App Shell` +Next implementation phase: `Phase 20 - Native Desktop Workflows` diff --git a/desktop/app/styles.css b/desktop/app/styles.css index 84eaf6a..9b47dee 100644 --- a/desktop/app/styles.css +++ b/desktop/app/styles.css @@ -1,17 +1,20 @@ :root { color-scheme: dark; - --bg: #151515; - --panel: #20211f; - --panel-strong: #292a26; - --line: #3a3d35; - --text: #f3f0e8; - --muted: #b5b0a4; - --moss: #8db67b; - --brass: #d2a64c; - --ember: #e0673f; - --aqua: #76c7c0; - --danger: #e95f5a; - --shadow: 0 24px 80px rgba(0, 0, 0, 0.28); + --bg: #10110f; + --bg-2: #171916; + --panel: rgba(28, 30, 27, 0.9); + --panel-2: rgba(38, 41, 36, 0.94); + --line: rgba(214, 207, 186, 0.14); + --line-strong: rgba(214, 207, 186, 0.28); + --text: #f4efe4; + --muted: #b8b19f; + --jade: #8ed6b3; + --aqua: #67c8cf; + --brass: #d7ae58; + --ember: #f0774f; + --danger: #ef6565; + --ink: #080907; + --shadow: 0 28px 100px rgba(0, 0, 0, 0.38); } * { @@ -22,13 +25,14 @@ body { margin: 0; min-width: 1024px; min-height: 100vh; - background: - linear-gradient(135deg, rgba(141, 182, 123, 0.14), transparent 36%), - radial-gradient(circle at 80% 8%, rgba(210, 166, 76, 0.16), transparent 30%), - var(--bg); color: var(--text); font-family: Avenir Next, Avenir, Helvetica Neue, sans-serif; letter-spacing: 0; + background: + linear-gradient(110deg, rgba(142, 214, 179, 0.12), transparent 34%), + linear-gradient(250deg, rgba(215, 174, 88, 0.12), transparent 42%), + repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.025) 0 1px, transparent 1px 72px), + linear-gradient(180deg, var(--bg), var(--bg-2)); } button, @@ -49,41 +53,42 @@ input { gap: 14px; padding: 18px 12px; border-right: 1px solid var(--line); - background: rgba(21, 21, 21, 0.86); - backdrop-filter: blur(20px); + background: rgba(10, 11, 9, 0.72); + backdrop-filter: blur(24px); } .brand-mark { display: grid; place-items: center; - width: 48px; - height: 48px; + width: 50px; + height: 50px; margin-bottom: 12px; - border: 1px solid rgba(141, 182, 123, 0.55); + border: 1px solid rgba(142, 214, 179, 0.42); border-radius: 8px; - background: linear-gradient(145deg, rgba(141, 182, 123, 0.2), rgba(118, 199, 192, 0.08)); - color: var(--moss); - font-weight: 800; + background: linear-gradient(145deg, rgba(142, 214, 179, 0.18), rgba(103, 200, 207, 0.08)); + color: var(--jade); + font-weight: 900; } .rail-button, .icon-button { display: grid; place-items: center; - width: 44px; - height: 44px; + width: 42px; + height: 42px; border: 1px solid transparent; border-radius: 8px; background: transparent; color: var(--muted); cursor: pointer; + font-weight: 800; } .rail-button:hover, .icon-button:hover, .rail-button.active { - border-color: var(--line); - background: var(--panel-strong); + border-color: var(--line-strong); + background: rgba(255, 255, 255, 0.07); color: var(--text); } @@ -105,6 +110,7 @@ input { .topbar { justify-content: space-between; + min-height: 98px; } .eyebrow, @@ -112,8 +118,9 @@ input { display: block; margin: 0 0 6px; color: var(--muted); - font-size: 12px; - font-weight: 700; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; text-transform: uppercase; } @@ -124,11 +131,12 @@ h2 { } h1 { - font-size: 34px; + font-size: 36px; + line-height: 1.04; } h2 { - font-size: 18px; + font-size: 20px; } .topbar-actions { @@ -140,22 +148,23 @@ h2 { align-items: center; min-height: 34px; padding: 0 12px; - border: 1px solid var(--line); + border: 1px solid rgba(215, 174, 88, 0.34); border-radius: 8px; color: var(--brass); - background: rgba(210, 166, 76, 0.08); + background: rgba(215, 174, 88, 0.08); font-size: 13px; - font-weight: 700; + font-weight: 800; } .status-pill.good { - color: var(--moss); - background: rgba(141, 182, 123, 0.1); + border-color: rgba(142, 214, 179, 0.36); + color: var(--jade); + background: rgba(142, 214, 179, 0.08); } .hero-grid { display: grid; - grid-template-columns: 1.15fr 1fr; + grid-template-columns: minmax(360px, 0.88fr) minmax(520px, 1.12fr); gap: 18px; } @@ -165,29 +174,41 @@ h2 { .panel { border: 1px solid var(--line); border-radius: 8px; - background: rgba(32, 33, 31, 0.9); + background: var(--panel); box-shadow: var(--shadow); + backdrop-filter: blur(20px); } .risk-orbit { - min-height: 164px; + min-height: 170px; gap: 22px; padding: 22px; + overflow: hidden; +} + +.risk-orbit::before { + content: ""; + width: 5px; + align-self: stretch; + border-radius: 8px; + background: linear-gradient(180deg, var(--jade), var(--brass), var(--ember)); } .score-ring { display: grid; place-items: center; - width: 116px; - height: 116px; + width: 118px; + height: 118px; + border: 1px solid rgba(240, 119, 79, 0.36); border-radius: 50%; - border: 10px solid rgba(224, 103, 63, 0.34); - background: linear-gradient(145deg, rgba(224, 103, 63, 0.18), rgba(210, 166, 76, 0.08)); + background: + linear-gradient(145deg, rgba(240, 119, 79, 0.18), rgba(215, 174, 88, 0.08)), + rgba(255, 255, 255, 0.03); } .score-ring span { font-size: 38px; - font-weight: 800; + font-weight: 900; } .score-ring small { @@ -198,12 +219,23 @@ h2 { .metric-strip { justify-content: space-between; - padding: 22px; + gap: 12px; + padding: 18px; +} + +.metric-strip > div { + min-width: 0; + flex: 1; + padding: 14px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(255, 255, 255, 0.035); } .metric-strip strong { display: block; - font-size: 32px; + overflow-wrap: anywhere; + font-size: 28px; } .muted { @@ -230,10 +262,10 @@ h2 { .file-drop { display: grid; place-items: center; - min-height: 112px; - border: 1px dashed rgba(118, 199, 192, 0.58); + min-height: 122px; + border: 1px dashed rgba(103, 200, 207, 0.55); border-radius: 8px; - background: rgba(118, 199, 192, 0.08); + background: rgba(103, 200, 207, 0.07); color: var(--aqua); font-weight: 800; cursor: pointer; @@ -257,8 +289,9 @@ h2 { .scan-controls label span, .switch span { color: var(--muted); - font-size: 12px; + font-size: 11px; font-weight: 800; + letter-spacing: 0.08em; text-transform: uppercase; } @@ -267,7 +300,7 @@ h2 { height: 42px; border: 1px solid var(--line); border-radius: 8px; - background: #171816; + background: rgba(8, 9, 7, 0.72); color: var(--text); padding: 0 12px; } @@ -289,13 +322,13 @@ h2 { } .primary-button { - background: var(--moss); - color: #11140f; + background: linear-gradient(135deg, var(--jade), var(--brass)); + color: var(--ink); } .ghost-button { border-color: var(--line); - background: transparent; + background: rgba(255, 255, 255, 0.055); color: var(--text); } @@ -319,7 +352,7 @@ h2 { .scenario-card { border: 1px solid var(--line); border-radius: 8px; - background: rgba(21, 21, 21, 0.46); + background: rgba(8, 9, 7, 0.42); padding: 12px; } @@ -361,12 +394,12 @@ h2 { } .timeline-item.done { - border-color: rgba(141, 182, 123, 0.6); - color: var(--moss); + border-color: rgba(142, 214, 179, 0.6); + color: var(--jade); } .timeline-item.active { - border-color: rgba(210, 166, 76, 0.76); + border-color: rgba(215, 174, 88, 0.76); color: var(--brass); } @@ -391,7 +424,9 @@ h2 { } .scenario-list, - .timeline { + .timeline, + .metric-strip { + display: grid; grid-template-columns: 1fr; } } diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..3b9171a --- /dev/null +++ b/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,7 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default desktop capability for Firmware Security Workbench", + "windows": ["main"], + "permissions": ["core:default", "opener:default"] +} diff --git a/docs/phase-19-nextgen-ui-packaging.md b/docs/phase-19-nextgen-ui-packaging.md new file mode 100644 index 0000000..10b93fe --- /dev/null +++ b/docs/phase-19-nextgen-ui-packaging.md @@ -0,0 +1,75 @@ +# Phase 19: Next-Gen UI + Desktop Packaging + +## Goal + +Phase 19 upgrades Firmware Security Workbench from a functional dashboard into a polished security console and adds the first GitHub-based desktop package workflow. + +The app now has a shared visual direction across the browser dashboard and desktop shell: dark operational workspace, compact evidence surfaces, Risk DNA cockpit, hardening studio, and release-grade desktop packaging path. + +## What This Phase Adds + +- Redesigned browser dashboard with mission-control layout. +- Redesigned desktop shell to match the dashboard visual system. +- Shared product direction for web and native desktop experience. +- GitHub Actions workflow for desktop builds on: + - macOS + - Windows + - Linux +- Tauri capability scaffold for native permissions. +- Test coverage for desktop shell files and packaging workflow presence. + +## How To Preview The Web Dashboard + +```bash +uvicorn backend.app:app --reload --port 8000 +``` + +Open: + +```text +http://127.0.0.1:8000/dashboard +``` + +## How To Preview The Desktop Shell + +```bash +cd desktop +python3 -m http.server 4173 --directory app +``` + +Open: + +```text +http://127.0.0.1:4173 +``` + +## How To Build Desktop Packages On GitHub + +Open GitHub Actions and run: + +```text +Desktop Packages +``` + +The workflow uploads desktop bundle artifacts for each operating system. + +You can also create a desktop package tag: + +```bash +git tag desktop-v0.1.0 +git push origin desktop-v0.1.0 +``` + +## Packaging Notes + +The current desktop app is an alpha shell that talks to the existing FastAPI backend when it is running. The next packaging milestone should bundle the scanner/runtime as a Tauri sidecar so the desktop app is fully self-contained. + +## Next Phase + +Phase 20 should add native workflows: + +- native file picker +- scan queue +- report viewer +- desktop-local scan storage +- bundled scanner sidecar diff --git a/frontend/dashboard.css b/frontend/dashboard.css index b6872c5..fe21e41 100644 --- a/frontend/dashboard.css +++ b/frontend/dashboard.css @@ -1,212 +1,357 @@ :root { - --bg: #ecf0ee; - --bg-band-a: #1b3040; - --bg-band-b: #2d5f68; - --surface: #ffffff; - --surface-soft: #f2f6f3; - --text: #14222a; - --muted: #4d6170; - --line: #c8d3d9; - --primary: #1f6075; - --primary-strong: #164e61; - --accent: #cf8c3b; - --danger: #9b2b2b; - --info: #1f5e93; + color-scheme: dark; + --bg: #10110f; + --bg-2: #171916; + --panel: rgba(28, 30, 27, 0.88); + --panel-2: rgba(38, 41, 36, 0.92); + --line: rgba(214, 207, 186, 0.14); + --line-strong: rgba(214, 207, 186, 0.28); + --text: #f4efe4; + --muted: #b8b19f; + --jade: #8ed6b3; + --aqua: #67c8cf; + --brass: #d7ae58; + --ember: #f0774f; + --danger: #ef6565; + --ink: #080907; + --shadow: 0 28px 100px rgba(0, 0, 0, 0.36); } * { box-sizing: border-box; } +html { + scroll-behavior: smooth; +} + body { margin: 0; + min-height: 100vh; color: var(--text); font-family: "IBM Plex Sans", "Segoe UI", sans-serif; + letter-spacing: 0; background: - radial-gradient(1200px 460px at 10% -10%, rgba(207, 140, 59, 0.18), transparent 70%), - linear-gradient(180deg, var(--bg-band-a) 0, var(--bg-band-b) 140px, var(--bg) 140px 100%); + linear-gradient(110deg, rgba(142, 214, 179, 0.12), transparent 34%), + linear-gradient(250deg, rgba(215, 174, 88, 0.12), transparent 42%), + repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.025) 0 1px, transparent 1px 72px), + linear-gradient(180deg, var(--bg), var(--bg-2)); } -.app-header { +button, +input { + font: inherit; +} + +.app-frame { + display: grid; + grid-template-columns: 76px minmax(0, 1fr); + min-height: 100vh; +} + +.side-rail { + position: sticky; + top: 0; display: flex; + flex-direction: column; align-items: center; - justify-content: space-between; - gap: 16px; - max-width: 1280px; + gap: 14px; + height: 100vh; + padding: 18px 12px; + border-right: 1px solid var(--line); + background: rgba(10, 11, 9, 0.72); + backdrop-filter: blur(24px); +} + +.brand-logo { + width: 50px; + height: 50px; + margin-bottom: 12px; +} + +.rail-action, +.icon-button { + display: inline-grid; + place-items: center; + width: 42px; + height: 42px; + border: 1px solid transparent; + border-radius: 8px; + background: transparent; + color: var(--muted); + text-decoration: none; + cursor: pointer; + font-weight: 800; +} + +.rail-action:hover, +.rail-action.active, +.icon-button:hover { + border-color: var(--line-strong); + background: rgba(255, 255, 255, 0.07); + color: var(--text); +} + +.workspace { + display: grid; + gap: 18px; + width: min(1480px, 100%); margin: 0 auto; - padding: 22px 20px 14px; + padding: 24px; } -.brand-cluster { +.app-header, +.panel-title-row, +.header-actions, +.button-row, +.metric-strip, +.risk-panel, +.checkbox-row { display: flex; align-items: center; - gap: 12px; } -.brand-logo { - width: 54px; - height: 54px; - flex: 0 0 54px; +.app-header { + justify-content: space-between; + gap: 24px; + min-height: 98px; } .brand-block h1 { margin: 0; - color: #f8fbfd; font-family: "Space Grotesk", "IBM Plex Sans", sans-serif; - font-size: clamp(1.4rem, 2.6vw, 2rem); - line-height: 1.2; + font-size: 36px; + line-height: 1.04; } .subtitle { - margin: 4px 0 0; - color: #d4e0e7; + max-width: 760px; + margin: 8px 0 0; + color: var(--muted); +} + +.eyebrow, +.metric-label, +.control-group span { + display: block; + margin: 0 0 6px; + color: var(--muted); + font-size: 11px; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; } .header-actions { - display: flex; - gap: 8px; + gap: 10px; } -.layout { - max-width: 1280px; - margin: 0 auto; - padding: 10px 20px 28px; +.mission-grid, +.primary-grid, +.evidence-grid { display: grid; - grid-template-columns: minmax(300px, 370px) minmax(460px, 1fr); - grid-template-areas: - "scan history" - "detail detail" - "assistant assistant"; - gap: 14px; + gap: 18px; } -.panel { - background: var(--surface); +.mission-grid { + grid-template-columns: minmax(360px, 0.88fr) minmax(520px, 1.12fr); +} + +.primary-grid { + grid-template-columns: minmax(360px, 0.78fr) minmax(560px, 1.22fr); +} + +.evidence-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.panel, +.risk-panel, +.metric-strip { border: 1px solid var(--line); border-radius: 8px; - padding: 14px; - box-shadow: 0 6px 20px rgba(10, 30, 45, 0.08); + background: var(--panel); + box-shadow: var(--shadow); + backdrop-filter: blur(20px); } -.panel h2 { - margin: 0 0 12px; - font-family: "Space Grotesk", "IBM Plex Sans", sans-serif; - font-size: 1.18rem; +.panel { + padding: 18px; } -.scan-panel { - grid-area: scan; +.risk-panel { + min-height: 170px; + gap: 22px; + padding: 22px; + overflow: hidden; } -.history-panel { - grid-area: history; +.risk-panel::before { + content: ""; + width: 5px; + align-self: stretch; + border-radius: 8px; + background: linear-gradient(180deg, var(--jade), var(--brass), var(--ember)); } -.detail-panel { - grid-area: detail; +.risk-core { + display: grid; + place-items: center; + width: 118px; + height: 118px; + border: 1px solid rgba(240, 119, 79, 0.36); + border-radius: 50%; + background: + linear-gradient(145deg, rgba(240, 119, 79, 0.18), rgba(215, 174, 88, 0.08)), + rgba(255, 255, 255, 0.03); } -.assistant-panel { - grid-area: assistant; +.risk-core span { + font-family: "Space Grotesk", sans-serif; + font-size: 28px; + font-weight: 800; } -.scan-form { - display: flex; - flex-direction: column; - gap: 10px; +.risk-core small { + color: var(--muted); + font-weight: 800; + text-transform: uppercase; } -.scan-form label { - font-size: 0.9rem; +.risk-panel strong, +.metric-tile strong { + display: block; + overflow-wrap: anywhere; } -.scan-form input[type="file"], -.scan-form input[type="number"], -.scan-form input[type="text"] { - width: 100%; - margin-top: 4px; +.risk-panel strong { + font-size: 24px; +} + +.metric-strip { + justify-content: space-between; + gap: 12px; + padding: 18px; +} + +.metric-tile { + min-width: 0; + flex: 1; + padding: 14px; border: 1px solid var(--line); - border-radius: 6px; - background: #fff; - color: var(--text); - padding: 9px 10px; + border-radius: 8px; + background: rgba(255, 255, 255, 0.035); +} + +.metric-tile strong { + font-size: 20px; +} + +h2, +h3 { + margin: 0; + font-family: "Space Grotesk", "IBM Plex Sans", sans-serif; +} + +h2 { + font-size: 20px; +} + +.panel-title-row { + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.scan-form { + display: grid; + gap: 14px; +} + +.file-drop { + display: grid; + place-items: center; + min-height: 122px; + border: 1px dashed rgba(103, 200, 207, 0.55); + border-radius: 8px; + color: var(--aqua); + background: rgba(103, 200, 207, 0.07); + cursor: pointer; + font-weight: 800; +} + +.file-drop input { + width: min(100%, 320px); + margin-top: 10px; } .controls-grid { display: grid; - grid-template-columns: 1fr; - gap: 8px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; } -.control-group span { - display: block; - font-size: 0.82rem; - color: var(--muted); +.control-group.wide { + grid-column: 1 / -1; +} + +input[type="file"], +input[type="number"], +input[type="text"] { + width: 100%; + min-height: 42px; + border: 1px solid var(--line); + border-radius: 8px; + background: rgba(8, 9, 7, 0.72); + color: var(--text); + padding: 9px 11px; } .checkbox-row { - display: flex; - align-items: center; gap: 8px; + color: var(--muted); } .button-row { - display: flex; - gap: 8px; flex-wrap: wrap; + gap: 10px; } -.icon-button, .primary-button, .secondary-button, .assist-chip { - border: 1px solid var(--line); - border-radius: 6px; - min-height: 36px; - padding: 0 12px; + min-height: 42px; + border: 1px solid transparent; + border-radius: 8px; + padding: 0 14px; cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - font-size: 0.9rem; - font-family: "IBM Plex Sans", sans-serif; -} - -.icon-button { - width: 36px; - background: var(--surface-soft); - color: var(--text); - text-decoration: none; + font-weight: 800; } .primary-button { - background: var(--primary); - color: #fff; - border-color: var(--primary-strong); + background: linear-gradient(135deg, var(--jade), var(--brass)); + color: var(--ink); } .secondary-button, .assist-chip { - background: var(--surface-soft); + border-color: var(--line); + background: rgba(255, 255, 255, 0.055); color: var(--text); } -.primary-button:hover { - background: var(--primary-strong); -} - +.primary-button:hover, .secondary-button:hover, -.icon-button:hover, .assist-chip:hover { - background: #e7efec; + filter: brightness(1.08); } .status-line { - min-height: 22px; - font-size: 0.85rem; - color: var(--muted); - padding-top: 4px; + min-height: 30px; + color: var(--aqua); + font-size: 13px; + font-weight: 700; + text-align: right; } .status-line.error { @@ -214,140 +359,110 @@ body { } .status-line.info { - color: var(--info); -} - -.panel-title-row { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; -} - -.panel-title-row h2 { - margin: 0; + color: var(--aqua); } -.history-wrap { +.history-wrap, +.detail-block, +.assistant-chat { overflow: auto; border: 1px solid var(--line); - border-radius: 6px; + border-radius: 8px; + background: rgba(8, 9, 7, 0.42); } table { width: 100%; + min-width: 660px; border-collapse: collapse; - min-width: 620px; } th, td { - padding: 8px 10px; + padding: 11px 12px; border-bottom: 1px solid var(--line); - font-size: 0.86rem; + color: var(--text); + font-size: 13px; text-align: left; } thead th { position: sticky; top: 0; - background: #eaf1f3; z-index: 1; + background: #252820; + color: var(--brass); } -tbody tr:hover { - background: #f1f7f8; +tbody tr { + cursor: pointer; } +tbody tr:hover, tbody tr.active { - background: #ddebee; -} - -.metric-grid { - display: grid; - grid-template-columns: repeat(4, minmax(130px, 1fr)); - gap: 8px; - margin-bottom: 12px; -} - -.metric { - border: 1px solid var(--line); - border-radius: 6px; - padding: 8px; - background: var(--surface-soft); -} - -.metric-label { - font-size: 0.75rem; - color: var(--muted); -} - -.metric-value { - margin-top: 4px; - font-weight: 600; - overflow-wrap: anywhere; -} - -.detail-columns { - display: grid; - grid-template-columns: repeat(2, minmax(200px, 1fr)); - gap: 10px; + background: rgba(142, 214, 179, 0.12); } .detail-block { - border: 1px solid var(--line); - border-radius: 6px; - padding: 8px; - min-height: 180px; - background: #fff; -} - -.detail-block h3 { - margin: 0 0 8px; - font-size: 0.9rem; + min-height: 290px; + padding: 12px; } .detail-block ul, .assistant-chat { margin: 0; - padding-left: 16px; - max-height: 250px; - overflow: auto; + padding-left: 18px; } .detail-block li { - margin-bottom: 6px; - font-size: 0.84rem; - line-height: 1.35; + margin-bottom: 9px; + font-size: 13px; + line-height: 1.45; overflow-wrap: anywhere; } +.status-pill { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 0 10px; + border: 1px solid rgba(142, 214, 179, 0.34); + border-radius: 8px; + color: var(--jade); + background: rgba(142, 214, 179, 0.08); + font-size: 12px; + font-weight: 800; +} + +.status-pill.cool { + border-color: rgba(103, 200, 207, 0.34); + color: var(--aqua); + background: rgba(103, 200, 207, 0.08); +} + .assistant-quick { display: flex; gap: 8px; flex-wrap: wrap; - margin-bottom: 8px; + margin-bottom: 12px; } .assistant-chat { + min-height: 180px; + max-height: 260px; + padding: 12px; list-style: none; - padding: 8px; - border: 1px solid var(--line); - border-radius: 6px; - min-height: 140px; - max-height: 240px; - background: #fafcfa; } .assistant-msg { - margin-bottom: 8px; - font-size: 0.86rem; - line-height: 1.4; + margin-bottom: 10px; + font-size: 14px; + line-height: 1.5; overflow-wrap: anywhere; } .assistant-msg.user { - color: var(--primary-strong); + color: var(--aqua); } .assistant-msg.bot { @@ -355,35 +470,59 @@ tbody tr.active { } .assistant-form { - margin-top: 8px; display: grid; grid-template-columns: 1fr auto; - gap: 8px; + gap: 10px; + margin-top: 12px; } -.assistant-form input[type="text"] { - width: 100%; - border: 1px solid var(--line); - border-radius: 6px; - padding: 9px 10px; - font-size: 0.9rem; +.muted { + color: var(--muted); } -@media (max-width: 980px) { - .layout { +@media (max-width: 1120px) { + .app-frame { + grid-template-columns: 1fr; + } + + .side-rail { + display: none; + } + + .workspace { + padding: 16px; + } + + .mission-grid, + .primary-grid, + .evidence-grid { grid-template-columns: 1fr; - grid-template-areas: - "scan" - "history" - "detail" - "assistant"; } - .metric-grid { - grid-template-columns: repeat(2, minmax(130px, 1fr)); + .metric-strip { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 640px) { + .brand-block h1 { + font-size: 28px; + } + + .app-header, + .risk-panel, + .metric-strip, + .assistant-form { + display: grid; } - .detail-columns { + .controls-grid, + .metric-strip { grid-template-columns: 1fr; } + + .header-actions { + justify-content: start; + } } diff --git a/frontend/dashboard.js b/frontend/dashboard.js index 6f0e45d..421c05b 100644 --- a/frontend/dashboard.js +++ b/frontend/dashboard.js @@ -17,6 +17,9 @@ const refs = { refreshHistory: document.getElementById("refresh-history"), loadHistory: document.getElementById("load-history"), clearDetail: document.getElementById("clear-detail-btn"), + missionRiskScore: document.getElementById("mission-risk-score"), + missionFileName: document.getElementById("mission-file-name"), + missionSummary: document.getElementById("mission-summary"), metricFile: document.getElementById("metric-file"), metricType: document.getElementById("metric-type"), metricEntropy: document.getElementById("metric-entropy"), @@ -611,11 +614,17 @@ function renderDetail(record) { const result = record?.result || {}; const file = result.file || {}; const analysis = result.analysis || {}; + const riskDna = analysis.risk_dna || {}; const findings = Array.isArray(analysis.suspicious_findings) ? analysis.suspicious_findings : []; const preview = Array.isArray(analysis.strings_preview) ? analysis.strings_preview : []; + refs.missionRiskScore.textContent = riskDna.score ?? String(analysis.suspicious_count ?? "-"); + refs.missionFileName.textContent = file.name || "Selected scan"; + refs.missionSummary.textContent = `Risk ${riskDna.band || "unknown"} / entropy ${ + analysis.entropy ?? "-" + } / findings ${analysis.suspicious_count ?? 0}`; refs.metricFile.textContent = file.name || "-"; refs.metricType.textContent = file.type_guess || "-"; refs.metricEntropy.textContent = @@ -653,6 +662,9 @@ function renderDetail(record) { function clearDetail() { state.selectedScanId = null; state.selectedRecord = null; + refs.missionRiskScore.textContent = "FWB"; + refs.missionFileName.textContent = "No scan selected"; + refs.missionSummary.textContent = "Run or select a scan to load live evidence."; refs.metricFile.textContent = "-"; refs.metricType.textContent = "-"; refs.metricEntropy.textContent = "-"; diff --git a/frontend/index.html b/frontend/index.html index 2a09959..5462a4b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,185 +6,217 @@ Firmware Security Workbench Dashboard -
      -
      +
      +
      -
      - - - - -
      -
      - -
      -
      -

      Scan Firmware

      - -
      - - - -
      - - - - - - - + S + H + E + A + + +
      +
      +
      +

      Firmware Security Workbench

      +

      Threat Intelligence Console

      +

      Local-first firmware scanning, Risk DNA, SBOM, CVE candidates, and hardening guidance.

      - - - -
      - - -
      - - -
      -
      - -
      -
      -

      Saved Scans

      - -
      - -
      - - - - - - - - - - - -
      IDUTCTypeFindingsFile
      -
      -
      - -
      -

      Scan Detail

      -
      -
      -
      File
      -
      -
      +
      + ? +
      -
      -
      Type
      -
      -
      + + +
      +
      +
      + FWB + console +
      +
      +

      Current target

      + No scan selected +

      Run or select a scan to load live evidence.

      +
      -
      -
      Entropy
      -
      -
      + +
      +
      + File + - +
      +
      + Type + - +
      +
      + Entropy + - +
      +
      + Findings + - +
      -
      -
      Findings
      -
      -
      +
      + +
      +
      +
      +
      +

      Input

      +

      Scan Firmware

      +
      + +
      + +
      + + +
      + + + + + + + +
      + +
      + + + +
      +
      +
      + +
      +
      +
      +

      Archive

      +

      Saved Scans

      +
      + +
      + +
      + + + + + + + + + + + +
      IDUTCTypeFindingsFile
      +
      +
      +
      + +
      +
      +
      +
      +

      Evidence

      +

      Top Findings

      +
      + Risk DNA ready +
      +
      +
        +
        +
        + +
        +
        +
        +

        Extracted

        +

        Strings Preview

        +
        + Static analysis +
        +
        +
          +
          +
          +
          + +
          +
          +
          +

          Copilot

          +

          Dashboard Assistant

          +
          +
          -
          -
          -
          -

          Top Findings

          -
            +
            + + + + + + + + + + + +
            -
            -

            Strings Preview

            -
              -
              -
              -
              - -
              -
              -

              Dashboard Assistant

              - -
              - -
              - - - - - - - - - - - - -
              - -
                - -
                - - -
                -
                -
                + +
                  + +
                  + + +
                  + + + diff --git a/tests/test_desktop_shell.py b/tests/test_desktop_shell.py index 1c11f45..733b6bf 100644 --- a/tests/test_desktop_shell.py +++ b/tests/test_desktop_shell.py @@ -16,9 +16,12 @@ def test_desktop_shell_files_exist(self) -> None: "desktop/app/main.js", "desktop/package.json", "desktop/src-tauri/tauri.conf.json", + "desktop/src-tauri/capabilities/default.json", "desktop/src-tauri/Cargo.toml", "desktop/src-tauri/src/main.rs", "docs/phase-18-desktop-app-shell.md", + "docs/phase-19-nextgen-ui-packaging.md", + ".github/workflows/desktop-packages.yml", ] for relative_path in expected_files: with self.subTest(path=relative_path): @@ -43,6 +46,15 @@ def test_tauri_config_points_to_desktop_app(self) -> None: self.assertEqual(config["build"]["frontendDist"], "../app") self.assertEqual(config["build"]["devUrl"], "http://127.0.0.1:4173") + def test_desktop_package_workflow_targets_three_operating_systems(self) -> None: + workflow = ( + self.repo_root / ".github" / "workflows" / "desktop-packages.yml" + ).read_text(encoding="utf-8") + self.assertIn("macos-latest", workflow) + self.assertIn("windows-latest", workflow) + self.assertIn("ubuntu-latest", workflow) + self.assertIn("actions/upload-artifact@v4", workflow) + if __name__ == "__main__": unittest.main() From 7c51877d27eb2802393740e16dff3b9b0c200024 Mon Sep 17 00:00:00 2001 From: YukiCodepth Date: Fri, 24 Apr 2026 14:32:09 +0530 Subject: [PATCH 4/5] Phase 19: complete UI wiring QA fixes --- backend/app.py | 42 +++++++++++++++++++++--- desktop/app/index.html | 16 ++++----- desktop/app/main.js | 14 ++++++++ tests/test_api_backend.py | 18 ++++++++++ tests/test_desktop_shell.py | 65 +++++++++++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 12 deletions(-) diff --git a/backend/app.py b/backend/app.py index a8c83a4..231f4a4 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4,6 +4,7 @@ from pathlib import Path from fastapi import FastAPI, File, Form, HTTPException, UploadFile +from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, Response from fastapi.staticfiles import StaticFiles @@ -21,6 +22,27 @@ def _db_path_from_param(db_path: str | None) -> Path: return Path(db_path) +def _apply_uploaded_file_name(result: dict[str, object], file_name: str) -> None: + if isinstance(result.get("file"), dict): + result["file"]["name"] = file_name + + sbom = result.get("sbom") + if not isinstance(sbom, dict): + return + + metadata = sbom.get("metadata") + if isinstance(metadata, dict): + component = metadata.get("component") + if isinstance(component, dict): + component["name"] = file_name + + components = sbom.get("components") + if isinstance(components, list) and components: + root_component = components[0] + if isinstance(root_component, dict) and root_component.get("bom-ref") == "firmware-root": + root_component["name"] = file_name + + def _scan_uploaded_file( uploaded_file: UploadFile, *, @@ -51,8 +73,10 @@ def _scan_uploaded_file( temp_path.unlink(missing_ok=True) uploaded_file.file.close() - if isinstance(result.get("file"), dict): - result["file"]["name"] = uploaded_file.filename or result["file"].get("name") + _apply_uploaded_file_name( + result, + uploaded_file.filename or str(result.get("file", {}).get("name", "upload.bin")), + ) return result @@ -78,8 +102,7 @@ def _scan_uploaded_payload( enable_rules=enable_rules, rules_dir=rules_dir, ) - if isinstance(result.get("file"), dict): - result["file"]["name"] = file_name + _apply_uploaded_file_name(result, file_name) return result except ScanError as exc: raise HTTPException(status_code=400, detail=str(exc)) from exc @@ -94,6 +117,17 @@ def _scan_uploaded_payload( description="Local API for firmware scanning and scan history", ) +app.add_middleware( + CORSMiddleware, + allow_origins=[ + "http://127.0.0.1:4173", + "http://localhost:4173", + ], + allow_credentials=False, + allow_methods=["GET", "POST", "OPTIONS"], + allow_headers=["*"], +) + PROJECT_ROOT = Path(__file__).resolve().parents[1] FRONTEND_DIR = PROJECT_ROOT / "frontend" diff --git a/desktop/app/index.html b/desktop/app/index.html index 54294a9..dd49049 100644 --- a/desktop/app/index.html +++ b/desktop/app/index.html @@ -10,16 +10,16 @@
                  @@ -36,7 +36,7 @@

                  Mission Control

                  -
                  +
                  69 @@ -69,7 +69,7 @@

                  Mission Control

                  -
                  +

                  Scan Studio

                  @@ -106,7 +106,7 @@

                  Evidence Feed

                    -
                    +

                    Hardening Studio

                    Projected 20 @@ -116,7 +116,7 @@

                    Hardening Studio

                    -
                    +

                    Release Timeline

                    v1.1 desktop foundation diff --git a/desktop/app/main.js b/desktop/app/main.js index b45fa42..b7b73ea 100644 --- a/desktop/app/main.js +++ b/desktop/app/main.js @@ -59,6 +59,7 @@ const refs = { scenarioList: document.getElementById("scenario-list"), actionList: document.getElementById("action-list") }; +const railButtons = Array.from(document.querySelectorAll(".rail-button")); function getApiBase() { return "http://127.0.0.1:8000"; @@ -158,6 +159,19 @@ refs.firmwareFile.addEventListener("change", () => { refs.fileLabel.textContent = file ? file.name : "Drop firmware or choose a file"; }); +for (const button of railButtons) { + button.addEventListener("click", () => { + const targetId = button.dataset.target; + const target = targetId ? document.getElementById(targetId) : null; + if (target) { + target.scrollIntoView({ behavior: "smooth", block: "start" }); + } + for (const item of railButtons) { + item.classList.toggle("active", item === button); + } + }); +} + refs.scanForm.addEventListener("submit", submitScan); refs.demoBtn.addEventListener("click", () => renderScan(demoScan)); refs.refreshBtn.addEventListener("click", checkApi); diff --git a/tests/test_api_backend.py b/tests/test_api_backend.py index ecd10db..bd6bf7f 100644 --- a/tests/test_api_backend.py +++ b/tests/test_api_backend.py @@ -38,6 +38,20 @@ def test_favicon_no_content(self) -> None: self.assertEqual(response.status_code, 204) self.assertEqual(response.content, b"") + def test_desktop_preview_origin_is_allowed(self) -> None: + response = self.client.options( + "/health", + headers={ + "Origin": "http://127.0.0.1:4173", + "Access-Control-Request-Method": "GET", + }, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.headers.get("access-control-allow-origin"), + "http://127.0.0.1:4173", + ) + def test_dashboard_entrypoint(self) -> None: response = self.client.get("/dashboard") self.assertEqual(response.status_code, 200) @@ -83,6 +97,8 @@ def test_scan_create_list_and_show(self) -> None: self.assertIn("hardening_simulation", created["analysis"]) self.assertIn("hardening_actions_count", created["analysis"]) self.assertIn("sbom", created) + self.assertEqual(created["sbom"]["metadata"]["component"]["name"], "api-demo.bin") + self.assertEqual(created["sbom"]["components"][0]["name"], "api-demo.bin") scan_id = created["storage"]["scan_id"] self.assertIsInstance(scan_id, int) @@ -147,6 +163,8 @@ def test_diff_endpoint(self) -> None: self.assertIn("old_scan", payload) self.assertIn("new_scan", payload) self.assertIn("diff", payload) + self.assertEqual(payload["old_scan"]["sbom"]["components"][0]["name"], "fw-old.bin") + self.assertEqual(payload["new_scan"]["sbom"]["components"][0]["name"], "fw-new.bin") self.assertTrue(payload["diff"]["summary"]["changed"]) self.assertIn("risk_shift", payload["diff"]) self.assertIn("hardening_shift", payload["diff"]) diff --git a/tests/test_desktop_shell.py b/tests/test_desktop_shell.py index 733b6bf..a9c7de6 100644 --- a/tests/test_desktop_shell.py +++ b/tests/test_desktop_shell.py @@ -2,9 +2,27 @@ import json import unittest +from html.parser import HTMLParser from pathlib import Path +class IdCollector(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.ids: set[str] = set() + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + for key, value in attrs: + if key == "id" and value is not None: + self.ids.add(value) + + +def collect_ids(html: str) -> set[str]: + parser = IdCollector() + parser.feed(html) + return parser.ids + + class DesktopShellTests(unittest.TestCase): def setUp(self) -> None: self.repo_root = Path(__file__).resolve().parents[1] @@ -35,6 +53,53 @@ def test_desktop_shell_contains_core_views(self) -> None: self.assertIn("Scan Studio", html) self.assertIn("Hardening Studio", html) self.assertIn("Release Timeline", html) + self.assertIn('data-target="mission-section"', html) + self.assertIn('data-target="scan-section"', html) + self.assertIn('data-target="hardening-section"', html) + self.assertIn('data-target="reports-section"', html) + + def test_desktop_shell_script_wires_interactive_controls(self) -> None: + script = (self.repo_root / "desktop" / "app" / "main.js").read_text( + encoding="utf-8" + ) + self.assertIn("railButtons", script) + self.assertIn("scrollIntoView", script) + self.assertIn("refs.scanForm.addEventListener", script) + self.assertIn("refs.demoBtn.addEventListener", script) + self.assertIn("refs.refreshBtn.addEventListener", script) + self.assertIn("refs.firmwareFile.addEventListener", script) + + def test_dashboard_dom_contains_all_javascript_targets(self) -> None: + html = (self.repo_root / "frontend" / "index.html").read_text(encoding="utf-8") + ids = collect_ids(html) + required_ids = { + "scan-form", + "firmware-file", + "min-string-length", + "max-strings", + "history-limit", + "db-path", + "save-scan", + "scan-status", + "history-body", + "refresh-history", + "load-history", + "clear-detail-btn", + "mission-risk-score", + "mission-file-name", + "mission-summary", + "metric-file", + "metric-type", + "metric-entropy", + "metric-findings", + "finding-list", + "strings-list", + "assistant-chat", + "assistant-form", + "assistant-input", + "clear-assistant-btn", + } + self.assertTrue(required_ids.issubset(ids)) def test_tauri_config_points_to_desktop_app(self) -> None: config = json.loads( From 9cad6355849ffc9be7c47bbdbee22ba0ee839600 Mon Sep 17 00:00:00 2001 From: YukiCodepth Date: Fri, 24 Apr 2026 14:55:09 +0530 Subject: [PATCH 5/5] Phase 19: fix desktop package workflow --- .github/workflows/desktop-packages.yml | 13 +- .gitignore | 1 + desktop/app-icon.svg | 46 +++++ desktop/package-lock.json | 232 +++++++++++++++++++++++++ desktop/package.json | 4 +- desktop/src-tauri/Cargo.toml | 2 +- desktop/src-tauri/tauri.conf.json | 2 +- docs/phase-19-nextgen-ui-packaging.md | 12 +- pyproject.toml | 1 + requirements.txt | 1 + tests/test_desktop_shell.py | 10 ++ 11 files changed, 316 insertions(+), 8 deletions(-) create mode 100644 desktop/app-icon.svg create mode 100644 desktop/package-lock.json diff --git a/.github/workflows/desktop-packages.yml b/.github/workflows/desktop-packages.yml index 15e4d6c..c784278 100644 --- a/.github/workflows/desktop-packages.yml +++ b/.github/workflows/desktop-packages.yml @@ -10,6 +10,7 @@ jobs: build-desktop: name: Build ${{ matrix.platform }} runs-on: ${{ matrix.platform }} + timeout-minutes: 45 strategy: fail-fast: false matrix: @@ -28,7 +29,10 @@ jobs: sudo apt-get update sudo apt-get install -y \ libwebkit2gtk-4.1-dev \ + libjavascriptcoregtk-4.1-dev \ + libsoup-3.0-dev \ libayatana-appindicator3-dev \ + libxdo-dev \ librsvg2-dev \ patchelf @@ -40,9 +44,16 @@ jobs: - name: Setup Rust uses: dtolnay/rust-toolchain@stable + - name: Print build tool versions + run: | + node --version + npm --version + cargo --version + rustc --version + - name: Install desktop dependencies working-directory: desktop - run: npm install + run: npm ci - name: Build desktop package working-directory: desktop diff --git a/.gitignore b/.gitignore index 4f90ea8..7e19957 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ node_modules/ dist/ build/ desktop/src-tauri/target/ +desktop/src-tauri/icons/ # Local output reports/generated/ diff --git a/desktop/app-icon.svg b/desktop/app-icon.svg new file mode 100644 index 0000000..36532f9 --- /dev/null +++ b/desktop/app-icon.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/package-lock.json b/desktop/package-lock.json new file mode 100644 index 0000000..7900ed6 --- /dev/null +++ b/desktop/package-lock.json @@ -0,0 +1,232 @@ +{ + "name": "firmware-security-workbench-desktop", + "version": "0.3.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "firmware-security-workbench-desktop", + "version": "0.3.0", + "devDependencies": { + "@tauri-apps/cli": "^2.0.0" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + } + } +} diff --git a/desktop/package.json b/desktop/package.json index 70a499a..10ee2f6 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,11 +1,13 @@ { "name": "firmware-security-workbench-desktop", - "version": "0.1.0", + "version": "0.3.0", "private": true, "type": "module", "scripts": { "dev": "tauri dev", + "prebuild": "tauri icon app-icon.svg", "build": "tauri build", + "icon": "tauri icon app-icon.svg", "preview": "python3 -m http.server 4173 --directory app" }, "devDependencies": { diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 86fb051..c058390 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "firmware-security-workbench" -version = "0.1.0" +version = "0.3.0" description = "Firmware Security Workbench desktop shell" authors = ["Firmware Security Workbench Contributors"] edition = "2021" diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 44790d5..c192f94 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Firmware Security Workbench", - "version": "0.1.0", + "version": "0.3.0", "identifier": "dev.fwb.desktop", "build": { "beforeDevCommand": "npm run preview", diff --git a/docs/phase-19-nextgen-ui-packaging.md b/docs/phase-19-nextgen-ui-packaging.md index 10b93fe..7cbaa99 100644 --- a/docs/phase-19-nextgen-ui-packaging.md +++ b/docs/phase-19-nextgen-ui-packaging.md @@ -16,6 +16,8 @@ The app now has a shared visual direction across the browser dashboard and deskt - Windows - Linux - Tauri capability scaffold for native permissions. +- Repeatable desktop dependency lockfile with `npm ci`. +- Branded desktop icon source that is regenerated before packaging. - Test coverage for desktop shell files and packaging workflow presence. ## How To Preview The Web Dashboard @@ -53,16 +55,18 @@ Desktop Packages The workflow uploads desktop bundle artifacts for each operating system. -You can also create a desktop package tag: +You can also create a desktop package tag. Use a fresh tag for each packaging attempt: ```bash -git tag desktop-v0.1.0 -git push origin desktop-v0.1.0 +git tag desktop-v0.3.0 +git push origin desktop-v0.3.0 ``` ## Packaging Notes -The current desktop app is an alpha shell that talks to the existing FastAPI backend when it is running. The next packaging milestone should bundle the scanner/runtime as a Tauri sidecar so the desktop app is fully self-contained. +The current desktop app is an alpha shell that talks to the existing FastAPI backend when it is running. The package workflow builds macOS, Windows, and Linux artifacts and regenerates platform icons from `desktop/app-icon.svg` before running Tauri. + +The next packaging milestone should bundle the scanner/runtime as a Tauri sidecar so the desktop app is fully self-contained. ## Next Phase diff --git a/pyproject.toml b/pyproject.toml index 9382915..57be6f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ authors = [ ] dependencies = [ "fastapi>=0.110.0", + "httpx>=0.27.0", "uvicorn>=0.25.0", "python-multipart>=0.0.9", ] diff --git a/requirements.txt b/requirements.txt index 2fe6898..52f434b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ fastapi>=0.110.0 +httpx>=0.27.0 uvicorn>=0.25.0 python-multipart>=0.0.9 diff --git a/tests/test_desktop_shell.py b/tests/test_desktop_shell.py index a9c7de6..7fedb95 100644 --- a/tests/test_desktop_shell.py +++ b/tests/test_desktop_shell.py @@ -32,7 +32,9 @@ def test_desktop_shell_files_exist(self) -> None: "desktop/app/index.html", "desktop/app/styles.css", "desktop/app/main.js", + "desktop/app-icon.svg", "desktop/package.json", + "desktop/package-lock.json", "desktop/src-tauri/tauri.conf.json", "desktop/src-tauri/capabilities/default.json", "desktop/src-tauri/Cargo.toml", @@ -120,6 +122,14 @@ def test_desktop_package_workflow_targets_three_operating_systems(self) -> None: self.assertIn("ubuntu-latest", workflow) self.assertIn("actions/upload-artifact@v4", workflow) + def test_desktop_build_generates_icons_before_packaging(self) -> None: + package_json = json.loads( + (self.repo_root / "desktop" / "package.json").read_text(encoding="utf-8") + ) + self.assertEqual(package_json["version"], "0.3.0") + self.assertEqual(package_json["scripts"]["prebuild"], "tauri icon app-icon.svg") + self.assertEqual(package_json["scripts"]["build"], "tauri build") + if __name__ == "__main__": unittest.main()