forked from mirrors/kingfisher
- Enhanced Access Map View: added fingerprint display, enabled searching by fingerprint, and implemented bidirectional navigation between Findings and Access Map nodes.
- Added Slack Access Map support with granular permissions in the tree view.
This commit is contained in:
parent
96f585ffa3
commit
8c07fb3f3c
4 changed files with 225 additions and 60 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# Kingfisher
|
||||
# Kingfisher
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/kingfisher_logo.png" alt="Kingfisher Logo" width="126" height="173" style="vertical-align: right;" />
|
||||
|
|
@ -13,8 +13,6 @@ It combines Intel’s SIMD-accelerated regex engine (Hyperscan) with language-aw
|
|||
|
||||
Designed for offensive security engineers and blue-teamers alike, Kingfisher helps you pivot across repo ecosystems, validate exposure paths, and hunt for developer-owned leaks that spill beyond the primary codebase.
|
||||
|
||||
For a look at how Kingfisher has grown from its early foundations into today's full-featured scanner, see [Lineage and Evolution](#lineage-and-evolution).
|
||||
|
||||
</p>
|
||||
|
||||
## Key Features
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ rules:
|
|||
id: kingfisher.openai.1
|
||||
pattern: |
|
||||
(?xi)
|
||||
\b
|
||||
(
|
||||
sk-[A-Z0-9]{48}
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 2
|
||||
min_entropy: 3.3
|
||||
|
|
@ -33,6 +35,7 @@ rules:
|
|||
id: kingfisher.openai.2
|
||||
pattern: |
|
||||
(?xi)
|
||||
\b
|
||||
(
|
||||
(sk-(?:proj|svcacct|None)-[A-Z0-9_-]{100,})
|
||||
)
|
||||
|
|
@ -65,9 +68,11 @@ rules:
|
|||
id: kingfisher.openai.3
|
||||
pattern: |
|
||||
(?xi)
|
||||
\b
|
||||
(
|
||||
sk-None-[A-Z0-9]{48}
|
||||
)
|
||||
\b
|
||||
pattern_requirements:
|
||||
min_digits: 2
|
||||
min_entropy: 3.3
|
||||
|
|
|
|||
|
|
@ -718,6 +718,11 @@
|
|||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#theme-toggle {
|
||||
background: #f9fafb;
|
||||
color: var(--brand);
|
||||
|
|
@ -853,8 +858,16 @@
|
|||
Findings
|
||||
</button>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<button class="btn" id="download-pdf" type="button" style="width:100%;">Download PDF report</button>
|
||||
<div class="nav-group" style="display:flex; flex-direction:column; gap:10px;">
|
||||
<button class="btn" id="download-findings-report" type="button" style="width:100%;">Download Findings Report</button>
|
||||
<div style="display:flex; flex-direction:column; gap:6px;">
|
||||
<span style="font-size:11px; text-transform:uppercase; letter-spacing:0.05em; color:var(--text-muted);">Findings scope</span>
|
||||
<select id="report-scope" class="rows-select">
|
||||
<option value="all" selected>All findings</option>
|
||||
<option value="filtered">Filtered findings</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn" id="download-access-report" type="button" style="width:100%;" disabled>Download Access Map Report</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
|
@ -1102,6 +1115,10 @@
|
|||
<label>Entropy</label>
|
||||
<div id="fd-entropy"></div>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<label>Validation Status</label>
|
||||
<div id="fd-validation-status"></div>
|
||||
</div>
|
||||
<div class="detail-field">
|
||||
<label>Git Commit</label>
|
||||
<div id="fd-commit"></div>
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -1714,9 +1740,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
|
||||
|
|
@ -1930,14 +1956,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
|
||||
? `<div style="font-size:12px; color:#475569; margin-top:4px;">
|
||||
<strong>Finding ID:</strong> ${escapeHtml(findingId || "-")} ·
|
||||
<strong>Token Name:</strong> ${escapeHtml(tokenName || "-")} ·
|
||||
<strong>User ID:</strong> ${escapeHtml(userId || "-")}
|
||||
</div>`
|
||||
: "";
|
||||
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 `<li><strong>Resources:</strong> ${resList}<br><strong>Permissions:</strong> ${permList}</li>`;
|
||||
})
|
||||
.join("");
|
||||
return `
|
||||
<li class="access-entry">
|
||||
<div class="access-head">
|
||||
${escapeHtml(entry.account || "(identity)")}
|
||||
<span class="tag">${escapeHtml((entry.provider || "Unknown").toUpperCase())}</span>
|
||||
</div>
|
||||
${metaLine}
|
||||
<ul class="access-groups">
|
||||
${groupList || "<li>No resources recorded.</li>"}
|
||||
</ul>
|
||||
</li>
|
||||
`;
|
||||
})
|
||||
.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 || {};
|
||||
|
|
@ -1952,15 +2069,16 @@
|
|||
return ra.localeCompare(rb);
|
||||
});
|
||||
|
||||
const counts = calculateValidationCounts();
|
||||
const counts = calculateValidationCounts(baseFindings);
|
||||
const durationSeconds = resolveScanDurationSeconds(rawData);
|
||||
const durationText = formatDurationText(durationSeconds);
|
||||
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
|
||||
|
|
@ -1969,49 +2087,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 `
|
||||
<tr>
|
||||
<td>${escapeHtml(rule.name || rule.id || "")}</td>
|
||||
<td>${escapeHtml(findingId || "")}</td>
|
||||
<td>${escapeHtml(finding.path || "")}</td>
|
||||
<td>${escapeHtml(statusRaw)}</td>
|
||||
<td>${escapeHtml(finding.confidence || "")}</td>
|
||||
<td>${finding.line != null ? escapeHtml(finding.line) : ""}</td>
|
||||
<td>${escapeHtml(gitUrl || "")}</td>
|
||||
<td>${escapeHtml(snippet.slice(0, 200))}</td>
|
||||
</tr>
|
||||
`;
|
||||
})
|
||||
.join("")
|
||||
: '<tr><td colspan="6">No findings available.</td></tr>';
|
||||
: '<tr><td colspan="7">No findings available.</td></tr>';
|
||||
|
||||
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 `<li><strong>Resources:</strong> ${resList}<br><strong>Permissions:</strong> ${permList}</li>`;
|
||||
})
|
||||
.join("");
|
||||
return `
|
||||
<li class="access-entry">
|
||||
<div class="access-head">
|
||||
${escapeHtml(entry.account || "(identity)")}
|
||||
<span class="tag">${escapeHtml((entry.provider || "Unknown").toUpperCase())}</span>
|
||||
</div>
|
||||
<ul class="access-groups">
|
||||
${groupList || "<li>No resources recorded.</li>"}
|
||||
</ul>
|
||||
</li>
|
||||
`;
|
||||
})
|
||||
.join("")
|
||||
? buildAccessMapListHtml()
|
||||
: "";
|
||||
|
||||
const accessSectionContent = hasAccess
|
||||
|
|
@ -2025,14 +2120,15 @@
|
|||
<meta charset="UTF-8">
|
||||
<title>Kingfisher Report</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 24px; color: #0f172a; }
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 16px; color: #0f172a; }
|
||||
h1, h2, h3 { margin: 0 0 12px; }
|
||||
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 18px; }
|
||||
.stat { padding: 12px; border: 1px solid #d1d5db; border-radius: 8px; background: #f8fafc; }
|
||||
.stat .label { font-size: 12px; text-transform: uppercase; color: #6b7280; letter-spacing: 0.05em; }
|
||||
.stat .value { font-size: 22px; font-weight: 700; margin-top: 6px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 8px; }
|
||||
th, td { border: 1px solid #d1d5db; padding: 8px 10px; font-size: 13px; text-align: left; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 8px; table-layout: fixed; }
|
||||
th, td { border: 1px solid #d1d5db; padding: 6px 8px; font-size: 11px; text-align: left; word-break: break-word; overflow-wrap: anywhere; }
|
||||
th { background: #e5e7eb; }
|
||||
.section { margin-bottom: 28px; }
|
||||
.tag { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #e0f2fe; color: #075985; font-weight: 700; font-size: 12px; margin-left: 8px; }
|
||||
|
|
@ -2049,12 +2145,12 @@
|
|||
</head>
|
||||
<body>
|
||||
<h1>Kingfisher Report</h1>
|
||||
<div class="meta">Generated ${escapeHtml(new Date().toLocaleString())}</div>
|
||||
<div class="meta">Generated ${escapeHtml(new Date().toLocaleString())} · Scope: ${escapeHtml(scopeLabel)}</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Dashboard</h2>
|
||||
<div class="grid">
|
||||
<div class="stat"><div class="label">Total Findings</div><div class="value">${findings.length}</div></div>
|
||||
<div class="stat"><div class="label">Total Findings</div><div class="value">${baseFindings.length}</div></div>
|
||||
<div class="stat"><div class="label">High Confidence</div><div class="value">${highConfidence}</div></div>
|
||||
<div class="stat"><div class="label">Active Credentials</div><div class="value">${counts.active || 0}</div></div>
|
||||
<div class="stat"><div class="label">Identities Mapped</div><div class="value">${accessMap.length}</div></div>
|
||||
|
|
@ -2072,20 +2168,16 @@
|
|||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Access Map</h2>
|
||||
${accessSectionContent}
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Findings (Active first)</h2>
|
||||
<h2>${escapeHtml(scope === "filtered" ? "Findings (Filtered, Active first)" : "Findings (Active first)")}</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rule</th>
|
||||
<th>Finding ID</th>
|
||||
<th>File Path</th>
|
||||
<th>Status</th>
|
||||
<th>Confidence</th>
|
||||
<th>Line</th>
|
||||
<th>Git URL</th>
|
||||
<th>Snippet</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
|
@ -2094,13 +2186,66 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const win = window.open("", "_blank", "width=1200,height=900");
|
||||
if (!win) {
|
||||
alert("Please allow pop-ups to download the PDF report.");
|
||||
alert("Please allow pop-ups to download the Findings report.");
|
||||
return;
|
||||
}
|
||||
win.document.write(pdfHtml);
|
||||
win.document.close();
|
||||
win.focus();
|
||||
setTimeout(() => win.print(), 400);
|
||||
}
|
||||
|
||||
function generateAccessMapReport() {
|
||||
if (!rawData) {
|
||||
alert("Load a report before downloading an Access Map report.");
|
||||
return;
|
||||
}
|
||||
|
||||
const accessListHtml = buildAccessMapListHtml({ includeMeta: true });
|
||||
if (!accessListHtml) {
|
||||
alert("No Access Map entries are available for this report.");
|
||||
return;
|
||||
}
|
||||
|
||||
const pdfHtml = `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Kingfisher Access Map Report</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; margin: 16px; color: #0f172a; }
|
||||
h1, h2 { margin: 0 0 12px; }
|
||||
.meta { color: #4b5563; font-size: 13px; margin-bottom: 14px; }
|
||||
.tag { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #e0f2fe; color: #075985; font-weight: 700; font-size: 12px; margin-left: 8px; }
|
||||
.access-list { list-style: none; padding-left: 0; margin: 0; display: flex; flex-direction: column; gap: 10px; }
|
||||
.access-entry { border: 1px solid #d1d5db; border-radius: 8px; padding: 10px; background: #f8fafc; }
|
||||
.access-head { font-weight: 700; font-size: 14px; margin-bottom: 6px; }
|
||||
.access-groups { margin: 0; padding-left: 16px; color: #1f2937; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Kingfisher Access Map Report</h1>
|
||||
<div class="meta">Generated ${escapeHtml(new Date().toLocaleString())}</div>
|
||||
<div class="section">
|
||||
<h2>Access Map</h2>
|
||||
<ul class="access-list">${accessListHtml}</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
const win = window.open("", "_blank", "width=1200,height=900");
|
||||
if (!win) {
|
||||
alert("Please allow pop-ups to download the Access Map report.");
|
||||
return;
|
||||
}
|
||||
win.document.write(pdfHtml);
|
||||
|
|
@ -2227,6 +2372,20 @@
|
|||
: "N/A";
|
||||
document.getElementById("fd-commit").textContent = commit;
|
||||
|
||||
const statusRaw =
|
||||
finding.validation && finding.validation.status ? String(finding.validation.status) : "Unknown";
|
||||
const normalizedStatus = normalizeValidationStatus(statusRaw);
|
||||
const badgeClass =
|
||||
normalizedStatus === "active"
|
||||
? "active"
|
||||
: normalizedStatus === "inactive"
|
||||
? "inactive"
|
||||
: "unknown";
|
||||
const statusEl = document.getElementById("fd-validation-status");
|
||||
if (statusEl) {
|
||||
statusEl.innerHTML = `<span class="status-badge ${badgeClass}">${escapeHtml(statusRaw)}</span>`;
|
||||
}
|
||||
|
||||
const path = finding.path || "";
|
||||
if (fdPathInput) {
|
||||
fdPathInput.value = path || "—";
|
||||
|
|
@ -2821,4 +2980,4 @@
|
|||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
|
@ -600,6 +600,9 @@ mod tests {
|
|||
|
||||
// This should not panic AND should correctly identify HTML
|
||||
let result = body_looks_like_html(&body, &headers);
|
||||
assert!(result, "Should correctly identify HTML even with multi-byte characters at boundary");
|
||||
assert!(
|
||||
result,
|
||||
"Should correctly identify HTML even with multi-byte characters at boundary"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue