diff --git a/README.md b/README.md index a156dcd..f92e38a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Kingfisher + # Kingfisher

Kingfisher Logo @@ -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). -

## Key Features diff --git a/data/rules/openai.yml b/data/rules/openai.yml index d77801e..b8c5933 100644 --- a/data/rules/openai.yml +++ b/data/rules/openai.yml @@ -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 diff --git a/docs/access-map-viewer/index.html b/docs/access-map-viewer/index.html index 5944f54..c4dd8af 100644 --- a/docs/access-map-viewer/index.html +++ b/docs/access-map-viewer/index.html @@ -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 - +
+ +
+
@@ -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 + ? `
+ 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} + +
  • + `; + }) + .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 ` ${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())} -
    - -
  • - `; - }) - .join("") + ? buildAccessMapListHtml() : ""; const accessSectionContent = hasAccess @@ -2025,14 +2120,15 @@ Kingfisher Report + + +

    Kingfisher Access Map Report

    +
    Generated ${escapeHtml(new Date().toLocaleString())}
    +
    +

    Access Map

    + +
    + + + `; + + 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 = `${escapeHtml(statusRaw)}`; + } + const path = finding.path || ""; if (fdPathInput) { fdPathInput.value = path || "—"; @@ -2821,4 +2980,4 @@ } - + \ No newline at end of file diff --git a/src/validation/httpvalidation.rs b/src/validation/httpvalidation.rs index ace6398..21aade7 100644 --- a/src/validation/httpvalidation.rs +++ b/src/validation/httpvalidation.rs @@ -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" + ); } }