Skip to content

Commit cf4c3e3

Browse files
authored
Merge pull request #109 from topstar-ai/feature/issue_98
XSS in search highlight, filter state lost on refresh, double scan on…
2 parents 1e952d9 + 349103b commit cf4c3e3

2 files changed

Lines changed: 86 additions & 31 deletions

File tree

src/fenn/dashboard/scanner.py

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,9 @@ def get_all_sessions(self) -> List[Dict[str, Any]]:
239239
sessions.append(parsed)
240240
return sessions
241241

242-
def get_overview(self) -> Dict[str, Any]:
243-
"""Aggregate stats for the dashboard home page."""
244-
sessions = self.get_all_sessions()
245-
246-
# Group by project
242+
@staticmethod
243+
def _build_projects_list(sessions: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
244+
"""Aggregate per-project stats from an already-loaded sessions list."""
247245
projects: Dict[str, Dict[str, Any]] = {}
248246
for s in sessions:
249247
name = s["project"]
@@ -265,25 +263,22 @@ def get_overview(self) -> Dict[str, Any]:
265263
p["running_count"] += 1
266264
elif s["status"] == "crashed":
267265
p["crashed_count"] += 1
266+
return sorted(projects.values(), key=lambda p: p["last_active"], reverse=True)
268267

269-
project_list = sorted(
270-
projects.values(), key=lambda p: p["last_active"], reverse=True
271-
)
272-
273-
total_warnings = sum(s["warning_count"] for s in sessions)
274-
total_exceptions = sum(s["exception_count"] for s in sessions)
275-
running = sum(1 for s in sessions if s["status"] == "running")
276-
crashed = sum(1 for s in sessions if s["status"] == "crashed")
268+
def get_overview(self) -> Dict[str, Any]:
269+
"""Aggregate stats for the dashboard home page."""
270+
sessions = self.get_all_sessions()
271+
project_list = self._build_projects_list(sessions)
277272

278273
return {
279274
"projects": project_list,
280275
"recent_sessions": sessions[:20],
281276
"total_sessions": len(sessions),
282-
"total_projects": len(projects),
283-
"total_warnings": total_warnings,
284-
"total_exceptions": total_exceptions,
285-
"running_sessions": running,
286-
"crashed_sessions": crashed,
277+
"total_projects": len(project_list),
278+
"total_warnings": sum(s["warning_count"] for s in sessions),
279+
"total_exceptions": sum(s["exception_count"] for s in sessions),
280+
"running_sessions": sum(1 for s in sessions if s["status"] == "running"),
281+
"crashed_sessions": sum(1 for s in sessions if s["status"] == "crashed"),
287282
"active_page": "home",
288283
}
289284

@@ -292,10 +287,8 @@ def get_project(self, project_name: str) -> Dict[str, Any]:
292287
all_sessions = self.get_all_sessions()
293288
sessions = [s for s in all_sessions if s["project"] == project_name]
294289

295-
overview = self.get_overview()
296-
297290
return {
298-
"projects": overview["projects"],
291+
"projects": self._build_projects_list(all_sessions),
299292
"project_name": project_name,
300293
"sessions": sessions,
301294
"total_sessions": len(sessions),

src/fenn/dashboard/static/app.js

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,26 @@
2727
}
2828
});
2929

30-
// Highlight search matches
30+
// Highlight search matches — DOM-only to avoid XSS via innerHTML
3131
if (query) {
3232
document.querySelectorAll(".log-entry:not(.hidden) .log-msg").forEach((el) => {
3333
const text = el.textContent;
34-
el.innerHTML = text.replace(
35-
new RegExp(escapeRegex(query), "gi"),
36-
(m) => `<span class="highlight">${m}</span>`
37-
);
34+
while (el.firstChild) el.removeChild(el.firstChild);
35+
const regex = new RegExp(escapeRegex(query), "gi");
36+
let last = 0, match;
37+
while ((match = regex.exec(text)) !== null) {
38+
if (match.index > last) el.appendChild(document.createTextNode(text.slice(last, match.index)));
39+
const span = document.createElement("span");
40+
span.className = "highlight";
41+
span.textContent = match[0];
42+
el.appendChild(span);
43+
last = match.index + match[0].length;
44+
}
45+
if (last < text.length) el.appendChild(document.createTextNode(text.slice(last)));
3846
});
3947
} else {
40-
document.querySelectorAll(".log-msg .highlight").forEach((el) => {
41-
el.outerHTML = el.textContent;
48+
document.querySelectorAll(".log-msg .highlight").forEach((span) => {
49+
span.replaceWith(document.createTextNode(span.textContent));
4250
});
4351
}
4452

@@ -136,16 +144,70 @@
136144
const project = sessionStatus.dataset.project;
137145
const sessionId = sessionStatus.dataset.session;
138146

147+
function createEntryEl(entry) {
148+
const row = document.createElement("div");
149+
row.className = `log-entry level-${entry.level} kind-${entry.kind}`;
150+
row.dataset.level = entry.level;
151+
row.dataset.kind = entry.kind;
152+
153+
const ts = document.createElement("span");
154+
ts.className = "log-ts";
155+
ts.textContent = entry.ts;
156+
157+
const kindWrap = document.createElement("span");
158+
kindWrap.className = "log-kind";
159+
const kindBadge = document.createElement("span");
160+
kindBadge.className = `badge badge-${entry.kind}`;
161+
kindBadge.style.fontSize = "10px";
162+
kindBadge.textContent = entry.kind;
163+
kindWrap.appendChild(kindBadge);
164+
165+
const levelWrap = document.createElement("span");
166+
levelWrap.className = "log-level";
167+
const levelBadge = document.createElement("span");
168+
levelBadge.className = `badge badge-${entry.level}`;
169+
levelBadge.style.fontSize = "10px";
170+
levelBadge.textContent = entry.level;
171+
levelWrap.appendChild(levelBadge);
172+
173+
const msg = document.createElement("span");
174+
msg.className = "log-msg";
175+
msg.textContent = entry.message;
176+
177+
row.append(ts, kindWrap, levelWrap, msg);
178+
return row;
179+
}
180+
139181
function refresh() {
140182
fetch(`/api/session/${encodeURIComponent(project)}/${encodeURIComponent(sessionId)}`)
141183
.then((r) => (r.ok ? r.json() : null))
142184
.then((data) => {
143185
if (!data) return;
144186

145-
// Reload if status changed or entry count changed
146-
const currentCount = document.querySelectorAll(".log-entry").length;
147-
if (data.entry_count !== currentCount || data.status !== "running") {
187+
// Status changed (e.g. running → completed/crashed) — full reload
188+
// because the header badge and meta row need server-side re-render.
189+
if (data.status !== "running") {
148190
location.reload();
191+
return;
192+
}
193+
194+
// New entries — append without disturbing filters or scroll position.
195+
const currentCount = document.querySelectorAll(".log-entry").length;
196+
if (data.entry_count !== currentCount && Array.isArray(data.entries)) {
197+
const container = document.querySelector(".log-entries");
198+
if (!container) return;
199+
// Remove the "no entries" empty-state placeholder if present.
200+
const empty = container.querySelector(".empty");
201+
if (empty) empty.remove();
202+
data.entries.slice(currentCount).forEach((entry) => {
203+
const row = createEntryEl(entry);
204+
// Respect current filter state so new rows appear/hide correctly.
205+
if (levelState[entry.level] === false || kindState[entry.kind] === false) {
206+
row.classList.add("hidden");
207+
}
208+
container.appendChild(row);
209+
});
210+
updateCount();
149211
}
150212
})
151213
.catch(() => {});

0 commit comments

Comments
 (0)