Skip to content

Commit 9fc5aef

Browse files
committed
Add the dashboard remediation simulator and cut the 1.0 release
The web dashboard now embeds the interactive remediation simulator: ticking a finding as fixed recomputes the projected grade and risk score live, mirroring the HTML report's simulator in the browser UI. Add a dashboard preview to the README and mark the project as a stable 1.0 release.
1 parent f7f7152 commit 9fc5aef

7 files changed

Lines changed: 161 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,22 @@ All notable changes to this project are documented here. The format is loosely
44
based on [Keep a Changelog](https://keepachangelog.com/), and the project aims
55
to follow semantic versioning once it reaches 1.0.
66

7+
## [1.0.0] - 2026-06-28
8+
9+
### Added
10+
11+
- Interactive remediation simulator in the web dashboard: tick a finding as
12+
fixed and the projected grade and risk score recompute live, the same idea as
13+
the HTML report's simulator, now in the browser dashboard.
14+
- A web-dashboard preview in the README.
15+
16+
### Changed
17+
18+
- First stable release. SentinelDeck now spans DNS, email authentication, HTTP,
19+
TLS, certificate transparency, technology fingerprinting, infrastructure
20+
intelligence, and threat intelligence, delivered through a colored CLI and a
21+
local web dashboard, passive by default with an opt-in active mode.
22+
723
## [0.9.0] - 2026-06-28
824

925
### Added

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Prefer a UI? `sentineldeck dashboard` opens an interactive web dashboard in your
4949
<td>
5050
<a href="https://pypi.org/project/sentineldeck/"><img alt="pypi" src="https://img.shields.io/pypi/v/sentineldeck?style=flat-square&labelColor=0a0a0f&color=dc2626&label=pypi"></a>
5151
<img alt="downloads" src="https://img.shields.io/pypi/dm/sentineldeck?style=flat-square&labelColor=0a0a0f&color=dc2626&label=downloads">
52-
<img alt="status" src="https://img.shields.io/badge/status-beta-dc2626?style=flat-square&labelColor=0a0a0f">
52+
<img alt="status" src="https://img.shields.io/badge/status-stable%201.0-dc2626?style=flat-square&labelColor=0a0a0f">
5353
<img alt="python" src="https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-dc2626?style=flat-square&labelColor=0a0a0f">
5454
<a href="LICENSE"><img alt="license" src="https://img.shields.io/badge/license-MIT-dc2626?style=flat-square&labelColor=0a0a0f"></a>
5555
</td>
@@ -74,6 +74,12 @@ It is built for the people who need that picture fast: an agency qualifying a
7474
prospect, a consultant producing a client report, or a small team checking its
7575
own footprint.
7676

77+
<p align="center">
78+
<img src="assets/dashboard.svg" alt="SentinelDeck web dashboard" width="840">
79+
</p>
80+
81+
<p align="center"><code>sentineldeck dashboard</code> opens this in your browser. Local only, zero extra dependencies, with a live remediation simulator that recomputes the grade as you tick fixes.</p>
82+
7783
## Contents
7884

7985
- [Features](#features)

assets/dashboard.svg

Lines changed: 94 additions & 0 deletions
Loading

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ license = {text = "MIT"}
1212
authors = [{name = "sanmaxdev"}]
1313
keywords = ["security", "attack-surface", "osint", "headers", "ssl", "sme"]
1414
classifiers = [
15-
"Development Status :: 4 - Beta",
15+
"Development Status :: 5 - Production/Stable",
1616
"Environment :: Console",
1717
"Intended Audience :: Information Technology",
1818
"Intended Audience :: System Administrators",

src/sentineldeck/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""SentinelDeck: passive attack-surface visibility for small businesses."""
22

3-
__version__ = "0.9.0"
3+
__version__ = "1.0.0"

src/sentineldeck/webui/app.js

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ const form = $("#scan-form"), input = $("#domain"), btn = $("#scan-btn");
55
const intro = $("#intro"), prog = $("#progress"), errBox = $("#error"), results = $("#results");
66
const SEV = { critical: "var(--crit)", high: "var(--high)", medium: "var(--medium)", low: "var(--low)", info: "var(--info)" };
77
const SEV_ORDER = ["critical", "high", "medium", "low", "info"];
8+
const POINTS = { critical: 40, high: 25, medium: 12, low: 5, info: 0 };
9+
const gradeFromScore = (s) => (s >= 80 ? "F" : s >= 60 ? "D" : s >= 40 ? "C" : s >= 20 ? "B" : "A");
810
let source = null;
11+
let SIM = [];
912

1013
$("#foot-version").textContent = "SentinelDeck dashboard";
1114
document.querySelectorAll(".chip").forEach((c) =>
@@ -101,24 +104,50 @@ function renderHero(report, findings) {
101104

102105
function renderFindings(findings) {
103106
const scored = findings.slice().sort((a, b) => SEV_ORDER.indexOf(a.severity) - SEV_ORDER.indexOf(b.severity));
104-
$("#findings").innerHTML = scored.map((f) => {
105-
const color = SEV[f.severity] || "var(--info)";
107+
SIM = scored.map((f) => ({
108+
f, fixed: false, pts: f.confidence === "indeterminate" ? 0 : (POINTS[f.severity] || 0),
109+
}));
110+
111+
const cards = SIM.map((item, i) => {
112+
const f = item.f, color = SEV[f.severity] || "var(--info)";
106113
const fix = f.remediation ? `
107114
<div class="fix-label">Fix${f.remediation.kind ? " &middot; " + esc(f.remediation.kind) : ""}</div>
108115
<pre>${esc(f.remediation.snippet)}</pre>` : "";
116+
const toggle = item.pts > 0
117+
? `<label class="fixtoggle"><input type="checkbox" data-i="${i}"> mark fixed</label>` : "";
109118
return `
110119
<div class="finding" style="border-left-color:${color}">
111-
<div class="finding-head" onclick="this.parentNode.querySelector('.finding-body').classList.toggle('hidden')">
120+
<div class="finding-head">
112121
<span class="sev-tag" style="background:${color};color:#0a0a0f">${esc(f.severity)}</span>
113-
<span class="finding-title">${esc(f.title)}</span>
122+
<span class="finding-title" onclick="this.closest('.finding').querySelector('.finding-body').classList.toggle('hidden')">${esc(f.title)}</span>
123+
${toggle}
114124
</div>
115125
<div class="finding-body hidden">
116126
<div>${esc(f.description)}</div>
117127
${f.recommendation ? `<div style="margin-top:8px">${esc(f.recommendation)}</div>` : ""}
118128
${fix}
119129
</div>
120130
</div>`;
121-
}).join("") || '<div class="muted">No findings.</div>';
131+
}).join("");
132+
133+
$("#findings").innerHTML = `<div class="sim" id="sim"></div>` + (cards || '<div class="muted">No findings.</div>');
134+
$("#findings").querySelectorAll(".fixtoggle input").forEach((cb) =>
135+
cb.addEventListener("change", () => { SIM[+cb.dataset.i].fixed = cb.checked; updateSim(); }));
136+
updateSim();
137+
}
138+
139+
function updateSim() {
140+
const projected = Math.min(100, SIM.filter((x) => !x.fixed).reduce((s, x) => s + x.pts, 0));
141+
const applied = SIM.filter((x) => x.fixed && x.pts > 0).length;
142+
const g = gradeFromScore(projected);
143+
const sim = document.getElementById("sim");
144+
if (!sim) return;
145+
if (!SIM.some((x) => x.pts > 0)) { sim.style.display = "none"; return; }
146+
sim.innerHTML =
147+
`<span class="sim-label">Remediation simulator</span>` +
148+
`<span class="sim-grade" style="color:var(--${g})">${g}</span>` +
149+
`<span class="sim-score">projected risk ${projected}/100</span>` +
150+
`<span class="muted">${applied ? applied + " fix" + (applied === 1 ? "" : "es") + " applied" : "tick findings to watch the grade improve"}</span>`;
122151
}
123152

124153
/* --- cards --------------------------------------------------------------- */

src/sentineldeck/webui/style.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,14 @@ main { max-width: 1180px; margin: 0 auto; padding: 28px; }
9292
.finding-body pre { background: #0c0d13; border: 1px solid var(--border); border-radius: 8px; padding: 12px 14px; overflow-x: auto; color: #cbd5e1; font-size: 12.5px; white-space: pre-wrap; }
9393
.fix-label { color: var(--red-soft); font-weight: 700; font-size: 12px; text-transform: uppercase; letter-spacing: .5px; margin: 12px 0 6px; }
9494

95+
/* Remediation simulator */
96+
.sim { display: flex; align-items: center; gap: 16px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 12px; padding: 14px 20px; margin-bottom: 14px; flex-wrap: wrap; }
97+
.sim-label { font-weight: 800; color: var(--red-soft); text-transform: uppercase; font-size: 11px; letter-spacing: 1px; }
98+
.sim-grade { font-size: 28px; font-weight: 900; line-height: 1; }
99+
.sim-score { font-weight: 700; }
100+
.fixtoggle { margin-left: auto; color: var(--muted); font-size: 12.5px; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; }
101+
.fixtoggle input { accent-color: #22c55e; width: 15px; height: 15px; }
102+
95103
/* Cards grid */
96104
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
97105
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 14px; padding: 18px 20px; }

0 commit comments

Comments
 (0)