File Path
@@ -1153,7 +1170,9 @@
const errorMsg = document.getElementById("error-msg");
const uploadSection = document.getElementById("upload-section");
const dashboard = document.getElementById("dashboard");
- const downloadPdfBtn = document.getElementById("download-pdf");
+ const downloadFindingsReportBtn = document.getElementById("download-findings-report");
+ const reportScopeSelect = document.getElementById("report-scope");
+ const downloadAccessReportBtn = document.getElementById("download-access-report");
const searchInput = document.getElementById("search-input");
const validationSelect = document.getElementById("validation-filter");
@@ -1273,8 +1292,14 @@
downloadJsonBtn.addEventListener("click", downloadFindingsJson);
downloadCsvBtn.addEventListener("click", downloadFindingsCsv);
copyAccessMapButton.addEventListener("click", copyFilteredAccessMap);
- if (downloadPdfBtn) {
- downloadPdfBtn.addEventListener("click", generatePdfReport);
+ if (downloadFindingsReportBtn) {
+ downloadFindingsReportBtn.addEventListener("click", () => {
+ const scope = reportScopeSelect ? reportScopeSelect.value : "all";
+ generatePdfReport(scope);
+ });
+ }
+ if (downloadAccessReportBtn) {
+ downloadAccessReportBtn.addEventListener("click", generateAccessMapReport);
}
if (themeToggle) {
themeToggle.addEventListener("click", () => {
@@ -1333,6 +1358,7 @@
function syncAccessMapUi(hasAccess) {
const previouslyAutoCollapsed = autoCollapsedAccessMap;
if (amToggle) amToggle.disabled = !hasAccess;
+ if (downloadAccessReportBtn) downloadAccessReportBtn.disabled = !hasAccess;
if (amEmptyNotice) {
amEmptyNotice.classList.toggle("hidden", hasAccess);
}
@@ -1583,10 +1609,6 @@
document.getElementById("stat-identities").textContent = (accessMap || []).length.toString();
- const durEl = document.getElementById("stat-duration");
- const scanSeconds = resolveScanDurationSeconds(rawData);
- durEl.textContent = formatDurationText(scanSeconds);
-
renderStatusChart(validationCounts);
}
@@ -1600,12 +1622,13 @@
const ruleName = (rule.name || rule.id || "").toLowerCase();
const path = (finding.path || "").toLowerCase();
const snippet = (finding.snippet || "").toLowerCase();
+ const fingerprint = (finding.fingerprint || "").toLowerCase();
const status = (finding.validation && finding.validation.status
? String(finding.validation.status)
: "").toLowerCase();
if (filterLower) {
- if (!ruleName.includes(filterLower) && !path.includes(filterLower) && !snippet.includes(filterLower)) {
+ if (!ruleName.includes(filterLower) && !path.includes(filterLower) && !snippet.includes(filterLower) && !fingerprint.includes(filterLower)) {
return false;
}
}
@@ -1713,9 +1736,9 @@
return "unknown";
}
- function calculateValidationCounts() {
+ function calculateValidationCounts(list = findings) {
const counts = { active: 0, inactive: 0, not_attempted: 0, unknown: 0 };
- findings.forEach((f) => {
+ (list || []).forEach((f) => {
const status =
f.finding && f.finding.validation && f.finding.validation.status
? f.finding.validation.status
@@ -1726,50 +1749,6 @@
return counts;
}
- function resolveScanDurationSeconds(data) {
- if (!data) return null;
- const candidates = [
- data.scan_duration,
- data.scanDuration,
- data.duration_seconds,
- data.duration,
- data.stats && data.stats.scan_duration,
- data.stats && data.stats.scanDuration,
- data.summary && data.summary.scan_duration,
- ];
-
- for (const candidate of candidates) {
- const parsed = parseDuration(candidate);
- if (parsed != null) return parsed;
- }
- return null;
- }
-
- function parseDuration(value) {
- if (value === undefined || value === null) return null;
- if (typeof value === "number" && Number.isFinite(value)) return value;
-
- const numeric = Number(value);
- if (Number.isFinite(numeric)) return numeric;
-
- if (typeof value === "string") {
- const match = value.match(/([\d.]+)\s*s/i);
- if (match) {
- const parsed = Number(match[1]);
- if (Number.isFinite(parsed)) return parsed;
- }
- }
- return null;
- }
-
- function formatDurationText(seconds) {
- if (seconds === null || seconds === undefined) return "-";
- const value = Number(seconds);
- if (!Number.isFinite(value)) return "-";
- if (value < 1) return value.toFixed(3) + "s";
- return value.toFixed(2) + "s";
- }
-
function renderStatusChart(counts) {
if (!statusChartCanvas) return;
const ctx = statusChartCanvas.getContext("2d");
@@ -1929,14 +1908,105 @@
triggerDownload("kingfisher-findings.csv", csv, "text/csv");
}
- function generatePdfReport() {
+ function getFindingIdFromFinding(finding) {
+ if (!finding) return "";
+ return (
+ finding.id ||
+ finding.finding_id ||
+ finding.findingId ||
+ finding.fingerprint ||
+ ""
+ );
+ }
+
+ function getFindingIdFromAccessEntry(entry) {
+ if (!entry) return "";
+ return (
+ entry.finding_id ||
+ entry.findingId ||
+ entry.finding ||
+ entry.fingerprint ||
+ ""
+ );
+ }
+
+ function getTokenNameFromAccessEntry(entry) {
+ if (!entry) return "";
+ return (
+ entry.token_name ||
+ entry.tokenName ||
+ (entry.token_details && entry.token_details.name) ||
+ (entry.token && entry.token.name) ||
+ ""
+ );
+ }
+
+ function getUserIdFromAccessEntry(entry) {
+ if (!entry) return "";
+ return (
+ entry.user_id ||
+ entry.userId ||
+ (entry.token_details && entry.token_details.user_id) ||
+ (entry.token && entry.token.user_id) ||
+ ""
+ );
+ }
+
+ function buildAccessMapListHtml({ includeMeta = false } = {}) {
+ if (!Array.isArray(accessMap) || accessMap.length === 0) {
+ return "";
+ }
+
+ return accessMap
+ .map((entry) => {
+ const groups = Array.isArray(entry.groups) ? entry.groups : [];
+ const findingId = getFindingIdFromAccessEntry(entry);
+ const tokenName = getTokenNameFromAccessEntry(entry);
+ const userId = getUserIdFromAccessEntry(entry);
+ const metaLine = includeMeta
+ ? `
+ Finding ID: ${escapeHtml(findingId || "-")} ·
+ Token Name: ${escapeHtml(tokenName || "-")} ·
+ User ID: ${escapeHtml(userId || "-")}
+
`
+ : "";
+ const groupList = groups
+ .map((g) => {
+ const resList = Array.isArray(g.resources) && g.resources.length
+ ? g.resources.map((r) => escapeHtml(String(r))).join(", ")
+ : "No resources listed";
+ const permList = Array.isArray(g.permissions) && g.permissions.length
+ ? g.permissions.map((p) => escapeHtml(String(p))).join(", ")
+ : "No permissions listed";
+ return `
Resources: ${resList}Permissions: ${permList}`;
+ })
+ .join("");
+ return `
+
+
+ ${escapeHtml(entry.account || "(identity)")}
+ ${escapeHtml((entry.provider || "Unknown").toUpperCase())}
+
+ ${metaLine}
+
+ ${groupList || "No resources recorded. "}
+
+
+ `;
+ })
+ .join("");
+ }
+
+ function generatePdfReport(scope = "all") {
if (!rawData) {
- alert("Load a report before downloading a PDF report.");
+ alert("Load a report before downloading a Findings report.");
return;
}
const statusOrder = { active: 0, inactive: 1, not_attempted: 2, unknown: 3 };
- const findingsForReport = Array.isArray(findings) ? findings.slice() : [];
+ const baseFindings =
+ scope === "filtered" ? getFilteredSortedFindings().slice() : (Array.isArray(findings) ? findings.slice() : []);
+ const findingsForReport = baseFindings.slice();
findingsForReport.sort((a, b) => {
const fa = a.finding || {};
const fb = b.finding || {};
@@ -1951,15 +2021,14 @@
return ra.localeCompare(rb);
});
- const counts = calculateValidationCounts();
- const durationSeconds = resolveScanDurationSeconds(rawData);
- const durationText = formatDurationText(durationSeconds);
+ const counts = calculateValidationCounts(baseFindings);
const hasAccess = Array.isArray(accessMap) && accessMap.length > 0;
- const statusImage = statusChartCanvas ? statusChartCanvas.toDataURL("image/png") : "";
- const highConfidence = findings.filter((f) => {
+ const statusImage = scope === "all" && statusChartCanvas ? statusChartCanvas.toDataURL("image/png") : "";
+ const highConfidence = baseFindings.filter((f) => {
const conf = f.finding && f.finding.confidence ? String(f.finding.confidence) : "";
return conf.toLowerCase() === "high";
}).length;
+ const scopeLabel = scope === "filtered" ? "Filtered findings" : "All findings";
const findingsHtml = findingsForReport.length
? findingsForReport
@@ -1968,49 +2037,26 @@
const finding = entry.finding || {};
const statusRaw = finding.validation && finding.validation.status ? finding.validation.status : "Unknown";
const status = normalizeValidationStatus(statusRaw);
+ const findingId = getFindingIdFromFinding(finding);
+ const gitUrl = getFileUrlFromFinding(finding);
const snippet = (finding.snippet || "").toString().replace(/\s+/g, " ").trim();
return `
${escapeHtml(rule.name || rule.id || "")}
+ ${escapeHtml(findingId || "")}
${escapeHtml(finding.path || "")}
${escapeHtml(statusRaw)}
- ${escapeHtml(finding.confidence || "")}
${finding.line != null ? escapeHtml(finding.line) : ""}
+ ${escapeHtml(gitUrl || "")}
${escapeHtml(snippet.slice(0, 200))}
`;
})
.join("")
- : '
No findings available. ';
+ : '
No findings available. ';
const accessListHtml = hasAccess
- ? accessMap
- .map((entry) => {
- const groups = Array.isArray(entry.groups) ? entry.groups : [];
- const groupList = groups
- .map((g) => {
- const resList = Array.isArray(g.resources) && g.resources.length
- ? g.resources.map((r) => escapeHtml(String(r))).join(", ")
- : "No resources listed";
- const permList = Array.isArray(g.permissions) && g.permissions.length
- ? g.permissions.map((p) => escapeHtml(String(p))).join(", ")
- : "No permissions listed";
- return `
Resources: ${resList}Permissions: ${permList}`;
- })
- .join("");
- return `
-
-
- ${escapeHtml(entry.account || "(identity)")}
- ${escapeHtml((entry.provider || "Unknown").toUpperCase())}
-
-
- ${groupList || "No resources recorded. "}
-
-
- `;
- })
- .join("")
+ ? buildAccessMapListHtml()
: "";
const accessSectionContent = hasAccess
@@ -2024,14 +2070,15 @@
Kingfisher Report
+
+
+
Kingfisher Access Map Report
+
Generated ${escapeHtml(new Date().toLocaleString())}
+
+
+