-
Findings scope
+
+
+
+
+
Findings + blast radius per credential. Suitable for bug bounty submissions.
+
+
+
+
+
-
+
+
Scan summary for tickets and remediation tracking.
+
@@ -1293,6 +1533,8 @@
let sortField = "rule";
let sortDirection = "asc";
let autoCollapsedAccessMap = false;
+ let currentDetailFinding = null;
+ let scanMetadata = {};
const dropZone = document.getElementById("drop-zone");
const fileInput = document.getElementById("file-input");
@@ -1301,9 +1543,11 @@
const errorMsg = document.getElementById("error-msg");
const uploadSection = document.getElementById("upload-section");
const dashboard = document.getElementById("dashboard");
- const downloadFindingsReportBtn = document.getElementById("download-findings-report");
+ const downloadRiskReportBtn = document.getElementById("download-risk-report");
+ const downloadScanReportBtn = document.getElementById("download-scan-report");
const reportScopeSelect = document.getElementById("report-scope");
const downloadAccessReportBtn = document.getElementById("download-access-report");
+ const exportFindingRiskBtn = document.getElementById("fd-export-risk-report");
const searchInput = document.getElementById("search-input");
const validationSelect = document.getElementById("validation-filter");
@@ -1423,15 +1667,29 @@
downloadJsonBtn.addEventListener("click", downloadFindingsJson);
downloadCsvBtn.addEventListener("click", downloadFindingsCsv);
copyAccessMapButton.addEventListener("click", copyFilteredAccessMap);
- if (downloadFindingsReportBtn) {
- downloadFindingsReportBtn.addEventListener("click", () => {
+ if (downloadRiskReportBtn) {
+ downloadRiskReportBtn.addEventListener("click", () => {
+ const activeOnly = document.getElementById("risk-active-only");
+ generateRiskReport({ activeOnly: activeOnly && activeOnly.checked });
+ });
+ }
+ if (downloadScanReportBtn) {
+ downloadScanReportBtn.addEventListener("click", () => {
const scope = reportScopeSelect ? reportScopeSelect.value : "all";
- generatePdfReport(scope);
+ const activeOnly = document.getElementById("scan-active-only");
+ generateScanReport(scope, { activeOnly: activeOnly && activeOnly.checked });
});
}
if (downloadAccessReportBtn) {
downloadAccessReportBtn.addEventListener("click", generateAccessMapReport);
}
+ if (exportFindingRiskBtn) {
+ exportFindingRiskBtn.addEventListener("click", () => {
+ if (currentDetailFinding) {
+ exportSingleFindingRiskReport(currentDetailFinding);
+ }
+ });
+ }
if (themeToggle) {
themeToggle.addEventListener("click", () => {
const current = document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark";
@@ -1628,6 +1886,8 @@
accessMap = [];
filteredAccessMapView = [];
rawData = null;
+ currentDetailFinding = null;
+ scanMetadata = {};
currentFilter = "";
validationFilter = "all";
@@ -1651,6 +1911,13 @@
renderFindingsTable();
updateMetrics();
+ const scanMetaSection = document.getElementById("scan-meta-section");
+ if (scanMetaSection) scanMetaSection.classList.add("hidden");
+ const blastSection = document.getElementById("fd-blast-radius");
+ if (blastSection) blastSection.classList.add("hidden");
+ const findingDetail = document.getElementById("finding-detail");
+ if (findingDetail) findingDetail.classList.add("hidden");
+
setActiveView("view-dashboard");
dashboard.classList.add("hidden");
uploadSection.classList.remove("hidden");
@@ -1712,8 +1979,10 @@
searchInput.value = "";
validationSelect.value = "all";
+ extractScanMetadata();
setActiveView("view-dashboard");
updateMetrics();
+ renderScanMetadata();
renderAccessMapTree();
renderFindingsTable();
@@ -1725,6 +1994,84 @@
setTimeout(() => loader.classList.add("hidden"), 250);
}
+ function extractScanMetadata() {
+ scanMetadata = {};
+ if (!rawData || typeof rawData !== "object") {
+ scanMetadata.timestamp = new Date().toLocaleString();
+ return;
+ }
+
+ // Try to extract metadata from the raw report data
+ const data = rawData;
+ scanMetadata.timestamp = data.timestamp || data.scan_timestamp || data.generated_at || new Date().toLocaleString();
+
+ // Target info
+ scanMetadata.target = data.target || data.scan_target || data.repository || data.repo ||
+ (data.stats && data.stats.target) ||
+ (data.summary && data.summary.target) || "";
+
+ // Duration
+ const duration = data.scan_duration || data.scanDuration ||
+ (data.stats && data.stats.duration) ||
+ (data.summary && data.summary.duration) || "";
+ if (duration) {
+ scanMetadata.duration = typeof duration === "number" ? (duration / 1000).toFixed(1) + "s" : String(duration);
+ }
+
+ // Version
+ scanMetadata.version = data.version || data.kingfisher_version ||
+ (data.kingfisher && data.kingfisher.version) || "";
+
+ // Bytes scanned
+ const bytes = data.bytes_scanned ||
+ (data.stats && data.stats.bytes_scanned) ||
+ (data.summary && data.summary.bytes_scanned) || 0;
+ if (bytes > 0) {
+ scanMetadata.bytesScanned = bytes >= 1048576
+ ? (bytes / 1048576).toFixed(1) + " MB"
+ : bytes >= 1024
+ ? (bytes / 1024).toFixed(1) + " KB"
+ : bytes + " B";
+ }
+ }
+
+ function renderScanMetadata() {
+ const section = document.getElementById("scan-meta-section");
+ if (!section) return;
+
+ const ts = document.getElementById("meta-timestamp");
+ const targetWrap = document.getElementById("meta-target-wrap");
+ const target = document.getElementById("meta-target");
+ const durationWrap = document.getElementById("meta-duration-wrap");
+ const duration = document.getElementById("meta-duration");
+ const versionWrap = document.getElementById("meta-version-wrap");
+ const version = document.getElementById("meta-version");
+
+ section.classList.remove("hidden");
+ if (ts) ts.textContent = scanMetadata.timestamp || new Date().toLocaleString();
+
+ if (scanMetadata.target) {
+ targetWrap.style.display = "";
+ target.textContent = scanMetadata.target;
+ } else {
+ targetWrap.style.display = "none";
+ }
+
+ if (scanMetadata.duration) {
+ durationWrap.style.display = "";
+ duration.textContent = scanMetadata.duration;
+ } else {
+ durationWrap.style.display = "none";
+ }
+
+ if (scanMetadata.version) {
+ versionWrap.style.display = "";
+ version.textContent = scanMetadata.version;
+ } else {
+ versionWrap.style.display = "none";
+ }
+ }
+
function updateMetrics() {
const totalFindings = findings.length;
document.getElementById("stat-total").textContent = totalFindings.toString();
@@ -2128,15 +2475,25 @@
.join("");
}
- function generatePdfReport(scope = "all") {
- if (!rawData) {
- alert("Load a report before downloading a Findings report.");
+ function generateScanReport(scope = "all", { activeOnly = false } = {}) {
+ if (!findings || findings.length === 0) {
+ alert("Load a report before downloading a Scan report.");
return;
}
const statusOrder = { active: 0, inactive: 1, not_attempted: 2, unknown: 3 };
- const baseFindings =
+ let baseFindings =
scope === "filtered" ? getFilteredSortedFindings().slice() : (Array.isArray(findings) ? findings.slice() : []);
+ if (activeOnly) {
+ baseFindings = baseFindings.filter((f) => {
+ const fd = f.finding || {};
+ return normalizeValidationStatus(fd.validation && fd.validation.status ? fd.validation.status : "") === "active";
+ });
+ if (baseFindings.length === 0) {
+ alert("No active credentials found for the selected scope. Uncheck 'Active credentials only' to include all findings.");
+ return;
+ }
+ }
const findingsForReport = baseFindings.slice();
findingsForReport.sort((a, b) => {
const fa = a.finding || {};
@@ -2159,7 +2516,27 @@
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 scopeLabel = (scope === "filtered" ? "Filtered findings" : "All findings") + (activeOnly ? " (active only)" : "");
+
+ // Gather unique rules for summary
+ const ruleSummary = {};
+ baseFindings.forEach((entry) => {
+ const rule = entry.rule || {};
+ const finding = entry.finding || {};
+ const key = rule.id || rule.name || "unknown";
+ if (!ruleSummary[key]) {
+ ruleSummary[key] = { name: rule.name || rule.id || "Unknown", count: 0, active: 0 };
+ }
+ ruleSummary[key].count++;
+ if (normalizeValidationStatus(finding.validation && finding.validation.status ? finding.validation.status : "") === "active") {
+ ruleSummary[key].active++;
+ }
+ });
+
+ const ruleSummaryRows = Object.values(ruleSummary)
+ .sort((a, b) => b.active - a.active || b.count - a.count)
+ .map((r) => `
| ${escapeHtml(r.name)} | ${r.count} | 0 ? "700" : "400"};">${r.active} |
`)
+ .join("");
const findingsHtml = findingsForReport.length
? findingsForReport
@@ -2167,113 +2544,155 @@
const rule = entry.rule || {};
const finding = entry.finding || {};
const statusRaw = finding.validation && finding.validation.status ? finding.validation.status : "Unknown";
- const status = normalizeValidationStatus(statusRaw);
+ const normalizedStatus = normalizeValidationStatus(statusRaw);
const findingId = getFindingIdFromFinding(finding);
const gitUrl = getFileUrlFromFinding(finding);
- const snippet = (finding.snippet || "").toString().replace(/\s+/g, " ").trim();
+ const statusColor = normalizedStatus === "active" ? "#dc2626" : normalizedStatus === "inactive" ? "#f97316" : "#6b7280";
return `
| ${escapeHtml(rule.name || rule.id || "")} |
- ${escapeHtml(findingId || "")} |
- ${escapeHtml(finding.path || "")} |
- ${escapeHtml(statusRaw)} |
- ${finding.line != null ? escapeHtml(finding.line) : ""} |
- ${escapeHtml(gitUrl || "")} |
- ${escapeHtml(snippet.slice(0, 200))} |
+ ${escapeHtml(findingId ? findingId.substring(0, 12) : "")} |
+ ${escapeHtml(finding.path || "")} |
+ ${escapeHtml(statusRaw)} |
+ ${escapeHtml(finding.confidence || "")} |
+ ${finding.line != null ? finding.line : ""} |
+ ${gitUrl ? `${escapeHtml(gitUrl.length > 50 ? gitUrl.substring(0, 50) + "..." : gitUrl)} | ` : " | "}
`;
})
.join("")
: '
| No findings available. |
';
- const accessListHtml = hasAccess
- ? buildAccessMapListHtml()
+ // Scan metadata section
+ const metaLines = [];
+ if (scanMetadata.timestamp) metaLines.push(`
Scan Date: ${escapeHtml(scanMetadata.timestamp)}`);
+ if (scanMetadata.target) metaLines.push(`
Target: ${escapeHtml(scanMetadata.target)}`);
+ if (scanMetadata.duration) metaLines.push(`
Duration: ${escapeHtml(scanMetadata.duration)}`);
+ if (scanMetadata.version) metaLines.push(`
Version: ${escapeHtml(scanMetadata.version)}`);
+ const metaHtml = metaLines.length
+ ? `
${metaLines.join('|')}
`
: "";
- const accessSectionContent = hasAccess
- ? `
`
- : "
No Access Map entries were found for this report.
";
+ // Executive summary
+ let execSummary = "";
+ if (counts.active > 0) {
+ execSummary = `
+ Action Required: This scan found ${counts.active} active credential${counts.active !== 1 ? "s" : ""} that validated successfully.
+ These credentials are live and should be rotated or revoked immediately.
+ ${highConfidence > 0 ? `${highConfidence} finding${highConfidence !== 1 ? "s" : ""} ${highConfidence !== 1 ? "are" : "is"} high confidence.` : ""}
+ ${hasAccess ? `Access mapping identified ${accessMap.length} identit${accessMap.length !== 1 ? "ies" : "y"} with resource access.` : ""}
+
`;
+ } else if (baseFindings.length > 0) {
+ execSummary = `
+ Review Recommended: This scan found ${baseFindings.length} finding${baseFindings.length !== 1 ? "s" : ""}.
+ ${counts.inactive > 0 ? `${counts.inactive} credential${counts.inactive !== 1 ? "s were" : " was"} inactive at scan time.` : ""}
+ ${counts.not_attempted > 0 ? `${counts.not_attempted} ${counts.not_attempted !== 1 ? "were" : "was"} not validated.` : ""}
+
`;
+ } else {
+ execSummary = `
+ Clean Scan: No findings detected in this scan.
+
`;
+ }
- const pdfHtml = `
-
-
-
-
-
Kingfisher Report
-
-
-
-
Kingfisher Report
-
Generated ${escapeHtml(new Date().toLocaleString())} · Scope: ${escapeHtml(scopeLabel)}
+ const pdfHtml = `
+
+
+
+
Kingfisher Scan Report
+
+
+
+
-
-
Dashboard
-
-
Total Findings
${baseFindings.length}
-
High Confidence
${highConfidence}
-
Active Credentials
${counts.active || 0}
-
Identities Mapped
${accessMap.length}
-
-
- ${statusImage ? `

` : ""}
-
- Active: ${counts.active || 0}
- Inactive: ${counts.inactive || 0}
- Not Attempted: ${counts.not_attempted || 0}
- Unknown: ${counts.unknown || 0}
-
-
-
+ ${metaHtml}
+ ${execSummary}
-
-
${escapeHtml(scope === "filtered" ? "Findings (Filtered, Active first)" : "Findings (Active first)")}
-
-
-
- | Rule |
- Finding ID |
- File Path |
- Status |
- Line |
- Git URL |
- Snippet |
-
-
-
- ${findingsHtml}
-
-
-
+
+
Summary
+
+
Total Findings
${baseFindings.length}
+
High Confidence
${highConfidence}
+
Active
${counts.active || 0}
+
Inactive
${counts.inactive || 0}
+
Not Validated
${counts.not_attempted || 0}
+
Identities
${(accessMap || []).length}
+
+
+ ${statusImage ? `

` : ""}
+
+ Active: ${counts.active || 0}
+ Inactive: ${counts.inactive || 0}
+ Not Attempted: ${counts.not_attempted || 0}
+ Unknown: ${counts.unknown || 0}
+
+
+
-
-
- `;
+ ${Object.keys(ruleSummary).length > 1 ? `
+
+
Findings by Rule
+
+ | Rule | Count | Active |
+ ${ruleSummaryRows}
+
+
+ ` : ""}
+
+
+
Detailed Findings
+
+
+
+ | Rule |
+ Fingerprint |
+ File Path |
+ Status |
+ Confidence |
+ Line |
+ Git URL |
+
+
+
+ ${findingsHtml}
+
+
+
+
+
+
+
+
+`;
const win = window.open("", "_blank", "width=1200,height=900");
if (!win) {
- alert("Please allow pop-ups to download the Findings report.");
+ alert("Please allow pop-ups to view the Scan report.");
return;
}
win.document.write(pdfHtml);
@@ -2283,49 +2702,168 @@
}
function generateAccessMapReport() {
- if (!rawData) {
- alert("Load a report before downloading an Access Map report.");
+ if (!Array.isArray(accessMap) || accessMap.length === 0) {
+ alert("No Access Map entries are available. Run a scan with --access-map first.");
return;
}
- const accessListHtml = buildAccessMapListHtml({ includeMeta: true });
- if (!accessListHtml) {
- alert("No Access Map entries are available for this report.");
- return;
- }
+ // Compute stats
+ let totalResources = 0;
+ let totalPermissions = 0;
+ const allPermSet = new Set();
+ const providerCounts = {};
+ accessMap.forEach((entry) => {
+ const prov = (entry.provider || "unknown").toUpperCase();
+ providerCounts[prov] = (providerCounts[prov] || 0) + 1;
+ (entry.groups || []).forEach((g) => {
+ totalResources += (g.resources || []).length;
+ (g.permissions || []).forEach((p) => allPermSet.add(p));
+ });
+ });
+ totalPermissions = allPermSet.size;
- const pdfHtml = `
-
-
-
-
-
Kingfisher Access Map Report
-
-
-
-
Kingfisher Access Map Report
-
Generated ${escapeHtml(new Date().toLocaleString())}
-
-
Access Map
-
+ // Build per-identity cards
+ const identityCards = accessMap.map((entry, idx) => {
+ const groups = Array.isArray(entry.groups) ? entry.groups : [];
+ const findingId = getFindingIdFromAccessEntry(entry);
+ const tokenName = getTokenNameFromAccessEntry(entry);
+ const userId = getUserIdFromAccessEntry(entry);
+ const provider = (entry.provider || "Unknown").toUpperCase();
+
+ // Token details
+ let tokenHtml = "";
+ if (entry.token_details) {
+ const td = entry.token_details;
+ const fields = [];
+ if (td.username) fields.push(["Username", td.username]);
+ if (td.token_type) fields.push(["Token Type", td.token_type]);
+ if (td.account_type) fields.push(["Account Type", td.account_type]);
+ if (td.name) fields.push(["Token Name", td.name]);
+ if (td.email) fields.push(["Email", td.email]);
+ if (td.created_at) fields.push(["Created", td.created_at]);
+ if (td.expires_at) fields.push(["Expires", td.expires_at]);
+ if (td.last_used_at) fields.push(["Last Used", td.last_used_at]);
+ if (Array.isArray(td.scopes) && td.scopes.length) fields.push(["Scopes", td.scopes.join(", ")]);
+ if (fields.length) {
+ tokenHtml = `
+
Token Details
+
+ ${fields.map(([k, v]) => `
${escapeHtml(k)}: ${escapeHtml(String(v))}
`).join("")}
+
+
`;
+ }
+ }
+
+ // Resource groups
+ const groupsHtml = groups.map((g) => {
+ const resources = g.resources || [];
+ const perms = g.permissions || [];
+ const resRows = resources.length
+ ? resources.map((r) => `
${escapeHtml(String(r))}
`).join("")
+ : '
No specific resources
';
+ const permTags = perms.length
+ ? perms.map((p) => `
${escapeHtml(p)}`).join(" ")
+ : '
No specific permissions';
+ return `
+
Resources (${resources.length})
+
${resRows}
+
Permissions (${perms.length})
+
${permTags}
+
`;
+ }).join("");
+
+ // Meta line
+ const metaParts = [];
+ if (findingId) metaParts.push(`
Fingerprint: ${escapeHtml(findingId.substring(0, 16))}`);
+ if (tokenName) metaParts.push(`
Token: ${escapeHtml(tokenName)}`);
+ if (userId) metaParts.push(`
User: ${escapeHtml(userId)}`);
+ const metaLine = metaParts.length
+ ? `
${metaParts.join(" · ")}
`
+ : "";
+
+ return `
+
+
👤
+
+
${escapeHtml(entry.account || "(identity)")}
+
${escapeHtml(provider)}
+
-
-
- `;
+ ${metaLine}
+ ${tokenHtml}
+ ${groupsHtml}
+
`;
+ }).join("");
+
+ // Provider summary
+ const providerSummaryRows = Object.entries(providerCounts)
+ .sort((a, b) => b[1] - a[1])
+ .map(([p, c]) => `
| ${escapeHtml(p)} | ${c} |
`)
+ .join("");
+
+ const pdfHtml = `
+
+
+
+
Kingfisher Access Map Report
+
+
+
+
+
+
+
Summary
+
+
Identities
${accessMap.length}
+
Total Resources
${totalResources}
+
Unique Permissions
${totalPermissions}
+
Providers
${Object.keys(providerCounts).length}
+
+ ${Object.keys(providerCounts).length > 1 ? `
+
+ | Provider | Identities |
+ ${providerSummaryRows}
+
+ ` : ""}
+
+
+
+
Identities & Access
+ ${identityCards}
+
+
+
+
+
+
+`;
const win = window.open("", "_blank", "width=1200,height=900");
if (!win) {
- alert("Please allow pop-ups to download the Access Map report.");
+ alert("Please allow pop-ups to view the Access Map report.");
return;
}
win.document.write(pdfHtml);
@@ -2573,6 +3111,10 @@
revokeBox.classList.add("hidden");
revokeCmd.textContent = "";
}
+
+ // Blast radius
+ currentDetailFinding = f;
+ renderBlastRadius(f);
}
function getFileUrlFromFinding(finding) {
@@ -2583,6 +3125,546 @@
return "";
}
+ function getAccessMapForFingerprint(fingerprint) {
+ if (!fingerprint || !Array.isArray(accessMap)) return [];
+ return accessMap.filter((entry) => entry.fingerprint === fingerprint);
+ }
+
+ function countResources(entries) {
+ let total = 0;
+ entries.forEach((entry) => {
+ (entry.groups || []).forEach((group) => {
+ total += (group.resources || []).length;
+ });
+ });
+ return total;
+ }
+
+ function countPermissions(entries) {
+ const permSet = new Set();
+ entries.forEach((entry) => {
+ (entry.groups || []).forEach((group) => {
+ (group.permissions || []).forEach((p) => permSet.add(p));
+ });
+ });
+ return permSet.size;
+ }
+
+ function generateRiskRationale(finding, accessEntries) {
+ const rule = finding.rule || {};
+ const fd = finding.finding || {};
+ const statusRaw = fd.validation && fd.validation.status ? String(fd.validation.status) : "";
+ const normalizedStatus = normalizeValidationStatus(statusRaw);
+ const isActive = normalizedStatus === "active";
+
+ if (!accessEntries || accessEntries.length === 0) {
+ if (isActive) {
+ return {
+ level: "medium",
+ text: `This is an active ${escapeHtml(rule.name || "credential")} found in ${escapeHtml(fd.path || "the codebase")}. No access map data is available, but the credential validated successfully, indicating it grants live access to the target service.`,
+ };
+ }
+ return {
+ level: "none",
+ text: "No access map data is linked to this finding. Run a scan with --access-map to map the blast radius of validated credentials.",
+ };
+ }
+
+ const providers = [...new Set(accessEntries.map((e) => (e.provider || "").toUpperCase()).filter(Boolean))];
+ const resourceCount = countResources(accessEntries);
+ const permCount = countPermissions(accessEntries);
+ const identityCount = accessEntries.length;
+
+ // Categorize resources
+ const resourceTypes = {};
+ accessEntries.forEach((entry) => {
+ (entry.groups || []).forEach((group) => {
+ (group.resources || []).forEach((r) => {
+ const rStr = String(r);
+ let type = "resource";
+ if (/s3|bucket|storage|blob/i.test(rStr)) type = "storage bucket";
+ else if (/lambda|function|cloud.run/i.test(rStr)) type = "serverless function";
+ else if (/iam|role|policy/i.test(rStr)) type = "IAM resource";
+ else if (/secret|vault|kms/i.test(rStr)) type = "secret/key";
+ else if (/ec2|instance|vm|compute/i.test(rStr)) type = "compute instance";
+ else if (/dynamodb|database|rds|sql/i.test(rStr)) type = "database";
+ else if (/repo|repository/i.test(rStr)) type = "repository";
+ resourceTypes[type] = (resourceTypes[type] || 0) + 1;
+ });
+ });
+ });
+
+ const typeDescriptions = Object.entries(resourceTypes)
+ .sort((a, b) => b[1] - a[1])
+ .map(([type, count]) => `${count} ${type}${count > 1 ? "s" : ""}`)
+ .join(", ");
+
+ // Determine risk level
+ let level = "low";
+ if (isActive && resourceCount > 10) level = "critical";
+ else if (isActive && resourceCount > 3) level = "high";
+ else if (isActive && resourceCount > 0) level = "medium";
+ else if (!isActive && resourceCount > 0) level = "low";
+
+ // Check for dangerous permissions
+ const allPerms = [];
+ accessEntries.forEach((e) => (e.groups || []).forEach((g) => allPerms.push(...(g.permissions || []))));
+ const dangerousPerms = allPerms.filter((p) =>
+ /admin|full.?control|owner|delete|write.*all|manage|superuser|\*/i.test(p)
+ );
+ if (isActive && dangerousPerms.length > 0) level = "critical";
+
+ // Check for token details with scopes
+ const scopeInfo = [];
+ accessEntries.forEach((entry) => {
+ if (entry.token_details && Array.isArray(entry.token_details.scopes)) {
+ scopeInfo.push(...entry.token_details.scopes);
+ }
+ });
+
+ let text = "";
+ if (isActive) {
+ text = `This active ${escapeHtml(rule.name || "credential")} grants access to
${resourceCount} resource${resourceCount !== 1 ? "s" : ""} across
${identityCount} identit${identityCount !== 1 ? "ies" : "y"} on ${providers.join(", ")}.`;
+ if (typeDescriptions) {
+ text += ` Accessible resources include ${typeDescriptions}.`;
+ }
+ if (dangerousPerms.length > 0) {
+ text += `
Warning: This credential has elevated privileges including ${dangerousPerms.slice(0, 3).map((p) => "
" + escapeHtml(p) + "").join(", ")}${dangerousPerms.length > 3 ? ` and ${dangerousPerms.length - 3} more` : ""}.`;
+ }
+ if (scopeInfo.length > 0) {
+ text += ` Token scopes: ${scopeInfo.slice(0, 5).map((s) => escapeHtml(s)).join(", ")}${scopeInfo.length > 5 ? ` (+${scopeInfo.length - 5} more)` : ""}.`;
+ }
+ } else {
+ text = `This ${escapeHtml(rule.name || "credential")} has access map data showing
${resourceCount} resource${resourceCount !== 1 ? "s" : ""} that would be accessible if the credential were active.`;
+ if (typeDescriptions) {
+ text += ` Resources include ${typeDescriptions}.`;
+ }
+ }
+
+ return { level, text };
+ }
+
+ function renderBlastRadius(f) {
+ const finding = f.finding || {};
+ const fingerprint = finding.fingerprint || "";
+ const blastSection = document.getElementById("fd-blast-radius");
+ const blastEntries = document.getElementById("fd-blast-entries");
+ const blastCount = document.getElementById("fd-blast-count");
+ const rationaleEl = document.getElementById("fd-risk-rationale");
+
+ const entries = getAccessMapForFingerprint(fingerprint);
+ const { level, text } = generateRiskRationale(f, entries);
+
+ // Always show the blast radius section
+ blastSection.classList.remove("hidden");
+
+ // Risk rationale
+ rationaleEl.className = "risk-rationale " + level;
+ rationaleEl.innerHTML = text;
+
+ // Count
+ const resourceTotal = countResources(entries);
+ blastCount.textContent = entries.length > 0
+ ? `${entries.length} identit${entries.length !== 1 ? "ies" : "y"}, ${resourceTotal} resource${resourceTotal !== 1 ? "s" : ""}`
+ : "No access data";
+
+ // Render entries
+ blastEntries.innerHTML = "";
+ if (entries.length === 0) return;
+
+ entries.forEach((entry) => {
+ const identity = document.createElement("div");
+ identity.className = "blast-identity";
+
+ const header = document.createElement("div");
+ header.className = "blast-identity__header";
+
+ const providerBadge = document.createElement("span");
+ providerBadge.className = "badge " + providerBadgeClass(entry.provider);
+ providerBadge.textContent = (entry.provider || "").toUpperCase();
+ header.appendChild(providerBadge);
+
+ const nameSpan = document.createElement("span");
+ nameSpan.textContent = entry.account || "(identity)";
+ header.appendChild(nameSpan);
+
+ // Token info summary
+ if (entry.token_details) {
+ const tokenInfo = document.createElement("span");
+ tokenInfo.style.fontSize = "12px";
+ tokenInfo.style.color = "var(--text-muted)";
+ const parts = [];
+ if (entry.token_details.username) parts.push(entry.token_details.username);
+ if (entry.token_details.token_type) parts.push(entry.token_details.token_type);
+ if (parts.length) tokenInfo.textContent = "(" + parts.join(" · ") + ")";
+ header.appendChild(tokenInfo);
+ }
+
+ identity.appendChild(header);
+
+ const resList = document.createElement("ul");
+ resList.className = "blast-resource-list";
+
+ (entry.groups || []).forEach((group) => {
+ const resources = group.resources || [];
+ const perms = group.permissions || [];
+
+ if (resources.length === 0 && perms.length > 0) {
+ const item = document.createElement("li");
+ item.className = "blast-resource-item";
+ const name = document.createElement("div");
+ name.className = "blast-resource-name";
+ name.textContent = "Project-wide / Unscoped";
+ item.appendChild(name);
+ const permDiv = document.createElement("div");
+ permDiv.className = "blast-perms";
+ perms.forEach((p) => {
+ const tag = document.createElement("span");
+ tag.className = "blast-perm-tag";
+ tag.textContent = p;
+ permDiv.appendChild(tag);
+ });
+ item.appendChild(permDiv);
+ resList.appendChild(item);
+ }
+
+ resources.forEach((resName) => {
+ const item = document.createElement("li");
+ item.className = "blast-resource-item";
+ const name = document.createElement("div");
+ name.className = "blast-resource-name";
+ name.textContent = String(resName);
+
+ // Add console link if available
+ const { resourceType, resourceName } = extractResourceParts(String(resName));
+ const consoleLink = buildResourceConsoleLink(entry.provider, resourceType, resourceName);
+ if (consoleLink) {
+ const a = document.createElement("a");
+ a.href = consoleLink;
+ a.target = "_blank";
+ a.rel = "noopener noreferrer";
+ a.textContent = " (open in console)";
+ a.style.fontSize = "11px";
+ a.style.fontFamily = "inherit";
+ a.style.color = "var(--brand)";
+ name.appendChild(a);
+ }
+
+ item.appendChild(name);
+
+ if (perms.length > 0) {
+ const permDiv = document.createElement("div");
+ permDiv.className = "blast-perms";
+ perms.forEach((p) => {
+ const tag = document.createElement("span");
+ tag.className = "blast-perm-tag";
+ tag.textContent = p;
+ permDiv.appendChild(tag);
+ });
+ item.appendChild(permDiv);
+ }
+ resList.appendChild(item);
+ });
+ });
+
+ identity.appendChild(resList);
+ blastEntries.appendChild(identity);
+ });
+ }
+
+ function exportSingleFindingRiskReport(f) {
+ const rule = f.rule || {};
+ const finding = f.finding || {};
+ const fingerprint = finding.fingerprint || "";
+ const entries = getAccessMapForFingerprint(fingerprint);
+ const { level, text: rationaleText } = generateRiskRationale(f, entries);
+ const statusRaw = finding.validation && finding.validation.status ? String(finding.validation.status) : "Unknown";
+ const gitUrl = getFileUrlFromFinding(finding);
+
+ const levelColors = {
+ critical: { bg: "#fef2f2", border: "#fca5a5", text: "#991b1b" },
+ high: { bg: "#fff7ed", border: "#fdba74", text: "#9a3412" },
+ medium: { bg: "#fffbeb", border: "#fde68a", text: "#92400e" },
+ low: { bg: "#f0fdf4", border: "#bbf7d0", text: "#166534" },
+ none: { bg: "#f8fafc", border: "#e2e8f0", text: "#475569" },
+ };
+ const colors = levelColors[level] || levelColors.none;
+
+ let accessHtml = "";
+ if (entries.length > 0) {
+ accessHtml = entries.map((entry) => {
+ const groups = (entry.groups || []).map((g) => {
+ const resList = (g.resources || []).map((r) => `
${escapeHtml(String(r))}`).join("");
+ const permList = (g.permissions || []).map((p) => `
${escapeHtml(p)}`).join(" ");
+ return `
+
Resources:
+
${resList || "- No specific resources
"}
+
Permissions:
+
${permList || "No specific permissions"}
+
`;
+ }).join("");
+
+ let tokenHtml = "";
+ if (entry.token_details) {
+ const td = entry.token_details;
+ const fields = [];
+ if (td.username) fields.push(["Username", td.username]);
+ if (td.token_type) fields.push(["Token Type", td.token_type]);
+ if (td.account_type) fields.push(["Account Type", td.account_type]);
+ if (td.name) fields.push(["Token Name", td.name]);
+ if (td.email) fields.push(["Email", td.email]);
+ if (td.created_at) fields.push(["Created", td.created_at]);
+ if (td.expires_at) fields.push(["Expires", td.expires_at]);
+ if (td.last_used_at) fields.push(["Last Used", td.last_used_at]);
+ if (Array.isArray(td.scopes) && td.scopes.length) fields.push(["Scopes", td.scopes.join(", ")]);
+ if (fields.length) {
+ tokenHtml = `
+
Token Details
+ ${fields.map(([k, v]) => `
${escapeHtml(k)}: ${escapeHtml(String(v))}
`).join("")}
+
`;
+ }
+ }
+
+ return `
+
+ ${escapeHtml((entry.provider || "").toUpperCase())}
+ ${escapeHtml(entry.account || "(identity)")}
+
+ ${groups}
+ ${tokenHtml}
+
`;
+ }).join("");
+ }
+
+ const reportHtml = `
+
+
+
+
Risk Report: ${escapeHtml(rule.name || rule.id || "Finding")}
+
+
+
+
Risk Report: ${escapeHtml(rule.name || rule.id || "")}
+
Generated ${escapeHtml(new Date().toLocaleString())} · Fingerprint: ${escapeHtml(fingerprint)}
+
+
Risk Assessment
+
+ ${escapeHtml(level)} risk
+ ${rationaleText}
+
+
+
Finding Details
+
+
${escapeHtml(rule.name || "")} (${escapeHtml(rule.id || "")})
+
${escapeHtml(statusRaw)}
+
${escapeHtml(finding.confidence || "")}
+
${finding.entropy != null ? escapeHtml(String(finding.entropy)) : "—"}
+
${escapeHtml(finding.path || "")}
+ ${gitUrl ? `
` : ""}
+ ${finding.git_metadata && finding.git_metadata.commit ? `
${escapeHtml(finding.git_metadata.commit.id ? finding.git_metadata.commit.id.substring(0, 8) : "")}
` : ""}
+ ${finding.git_metadata && finding.git_metadata.commit && finding.git_metadata.commit.committer ? `
${escapeHtml(finding.git_metadata.commit.committer.email || "")}
` : ""}
+
+
+
Match Snippet
+
${escapeHtml(finding.snippet || "")}
+
+ ${entries.length > 0 ? `
+
Blast Radius (${entries.length} Identit${entries.length !== 1 ? "ies" : "y"}, ${countResources(entries)} Resource${countResources(entries) !== 1 ? "s" : ""})
+ ${accessHtml}
+ ` : `
+
Blast Radius
+
No access map data available. Run with --access-map to map credential blast radius.
+ `}
+
+
+
+
+
+`;
+
+ const win = window.open("", "_blank", "width=1000,height=900");
+ if (!win) {
+ alert("Please allow pop-ups to open the risk report.");
+ return;
+ }
+ win.document.write(reportHtml);
+ win.document.close();
+ win.focus();
+ }
+
+ function generateRiskReport({ activeOnly = false } = {}) {
+ if (!findings || findings.length === 0) {
+ alert("No findings loaded. Load a report first.");
+ return;
+ }
+
+ let pool = findings.slice();
+ if (activeOnly) {
+ pool = pool.filter((f) => {
+ const fd = f.finding || {};
+ return normalizeValidationStatus(fd.validation && fd.validation.status ? fd.validation.status : "") === "active";
+ });
+ if (pool.length === 0) {
+ alert("No active credentials found. Uncheck 'Active credentials only' to include all findings.");
+ return;
+ }
+ }
+
+ // Sort: active first, then by rule name
+ const statusOrder = { active: 0, inactive: 1, not_attempted: 2, unknown: 3 };
+ const sorted = pool.sort((a, b) => {
+ const fa = a.finding || {};
+ const fb = b.finding || {};
+ const keyA = normalizeValidationStatus(fa.validation && fa.validation.status ? fa.validation.status : "");
+ const keyB = normalizeValidationStatus(fb.validation && fb.validation.status ? fb.validation.status : "");
+ const sa = statusOrder.hasOwnProperty(keyA) ? statusOrder[keyA] : 4;
+ const sb = statusOrder.hasOwnProperty(keyB) ? statusOrder[keyB] : 4;
+ if (sa !== sb) return sa - sb;
+ const ra = (a.rule && (a.rule.name || a.rule.id)) || "";
+ const rb = (b.rule && (b.rule.name || b.rule.id)) || "";
+ return ra.localeCompare(rb);
+ });
+
+ const counts = calculateValidationCounts(sorted);
+ const riskScopeLabel = activeOnly ? "Active credentials only" : "All findings";
+ const findingsWithAccess = sorted.filter((f) => {
+ const fp = f.finding && f.finding.fingerprint ? f.finding.fingerprint : "";
+ return getAccessMapForFingerprint(fp).length > 0;
+ });
+
+ const levelColors = {
+ critical: { bg: "#fef2f2", border: "#fca5a5", text: "#991b1b" },
+ high: { bg: "#fff7ed", border: "#fdba74", text: "#9a3412" },
+ medium: { bg: "#fffbeb", border: "#fde68a", text: "#92400e" },
+ low: { bg: "#f0fdf4", border: "#bbf7d0", text: "#166534" },
+ none: { bg: "#f8fafc", border: "#e2e8f0", text: "#475569" },
+ };
+
+ let findingSections = "";
+ sorted.forEach((f, idx) => {
+ const rule = f.rule || {};
+ const finding = f.finding || {};
+ const fp = finding.fingerprint || "";
+ const entries = getAccessMapForFingerprint(fp);
+ const { level, text: rationaleText } = generateRiskRationale(f, entries);
+ const colors = levelColors[level] || levelColors.none;
+ const statusRaw = finding.validation && finding.validation.status ? String(finding.validation.status) : "Unknown";
+ const gitUrl = getFileUrlFromFinding(finding);
+
+ let accessSummary = "";
+ if (entries.length > 0) {
+ const resCount = countResources(entries);
+ const permCount = countPermissions(entries);
+ accessSummary = entries.map((entry) => {
+ const resources = [];
+ (entry.groups || []).forEach((g) => {
+ (g.resources || []).forEach((r) => resources.push(String(r)));
+ });
+ const perms = [];
+ (entry.groups || []).forEach((g) => (g.permissions || []).forEach((p) => perms.push(p)));
+ const uniquePerms = [...new Set(perms)];
+ return `
+
+ ${escapeHtml((entry.provider || "").toUpperCase())}
+ ${escapeHtml(entry.account || "")}
+
+ ${resources.length ? `
Resources (${resources.length}): ${resources.slice(0, 8).map((r) => "" + escapeHtml(r) + "").join(", ")}${resources.length > 8 ? ` +${resources.length - 8} more` : ""}
` : ""}
+ ${uniquePerms.length ? `
Permissions (${uniquePerms.length}): ${uniquePerms.slice(0, 6).map((p) => escapeHtml(p)).join(", ")}${uniquePerms.length > 6 ? ` +${uniquePerms.length - 6} more` : ""}
` : ""}
+
`;
+ }).join("");
+ }
+
+ findingSections += `
+
+
+
${idx + 1}. ${escapeHtml(rule.name || rule.id || "Unknown Rule")}
+
${escapeHtml(level)}
+
+
+ ${rationaleText}
+
+
+
Path: ${escapeHtml(finding.path || "—")}
+
Line: ${finding.line != null ? finding.line : "—"}
+
Validation: ${escapeHtml(statusRaw)}
+
Confidence: ${escapeHtml(finding.confidence || "—")}
+ ${gitUrl ? `
` : ""}
+
+
${escapeHtml((finding.snippet || "").substring(0, 300))}
+ ${accessSummary ? `
Blast Radius
${accessSummary}
` : ""}
+
`;
+ });
+
+ const reportHtml = `
+
+
+
+
Kingfisher Risk Report
+
+
+
+
Kingfisher Risk Report
+
+ Generated ${escapeHtml(new Date().toLocaleString())} ·
+ Scope: ${escapeHtml(riskScopeLabel)} ·
+ ${sorted.length} finding${sorted.length !== 1 ? "s" : ""} ·
+ ${findingsWithAccess.length} with access map data
+
+
+
+
Total Findings
${sorted.length}
+
Active Credentials
${counts.active || 0}
+
With Blast Radius
${findingsWithAccess.length}
+
Identities Mapped
${(accessMap || []).length}
+
+
+
Findings by Risk
+ ${findingSections}
+
+
+
+
+
+`;
+
+ const win = window.open("", "_blank", "width=1100,height=900");
+ if (!win) {
+ alert("Please allow pop-ups to view the risk report.");
+ return;
+ }
+ win.document.write(reportHtml);
+ win.document.close();
+ win.focus();
+ }
+
function renderAccessMapTree(filter = "") {
const root = document.getElementById("am-tree-root");
root.innerHTML = "";