include db for gitea build
Build and Publish / Metadata (push) Successful in 10s
Build and Publish / BuildAndDeployAmd64 (push) Successful in 2m16s

This commit is contained in:
mats committed 2026-09-20 11:01:56 +02:00
1 parent 8de4a2e010
commit 645f923175
33 files changed
+1490 -150

No files matched your search

+77 -13
View File
@@ -15,6 +15,7 @@ import {
RotateCcw,
ScrollText,
ShieldBan,
Bot,
Sparkles,
Loader2,
X,
@@ -38,7 +39,7 @@ import { IpButton } from "./common/IpButton"
import { IssueOccurrences } from "./IssueOccurrences"
import { NeedleTestModal } from "./NeedleTestModal"
import { FilterBar, type FilterChip } from "./FilterBar"
import { UA_CLASSES, uaClassLabel } from "../lib/ua-class"
import { CRAWLER_CLASSES, UA_CLASSES, uaClassLabel } from "../lib/ua-class"
import type { Issue, IssueSample } from "../lib/types"
export const kindMeta: Record<string, { label: string; Icon: typeof Activity }> = {
@@ -64,6 +65,20 @@ interface HoneypotIP {
promoted_until: string
}
interface BlockerStatus {
enabled: boolean
dryRun: boolean
candidates?: string[]
managed?: Record<string, number>
}
/** An IP flagged by the honeypot tracker or the ingress blocker. */
interface DetectedIP {
ip: string
sources: ("honeypot" | "blocker")[]
count?: number
}
interface DetectorTestResult {
passed?: boolean
scenario?: string
@@ -684,6 +699,7 @@ export const IssuesVisualizer = ({
const [clientExclude, setClientExclude] = useState<Set<string>>(new Set())
const [showSpamIPs, setShowSpamIPs] = useState(false)
const [spamIPs, setSpamIPs] = useState<HoneypotIP[]>([])
const [blockerStatus, setBlockerStatus] = useState<BlockerStatus | null>(null)
const [spamIPsLoading, setSpamIPsLoading] = useState(false)
const [spamIPsError, setSpamIPsError] = useState<string | null>(null)
@@ -694,6 +710,9 @@ export const IssuesVisualizer = ({
const response = await authFetch("/api/honeypot/ips")
if (!response.ok) throw new Error(`HTTP ${response.status}`)
setSpamIPs((await response.json()) as HoneypotIP[])
// The blocker route only exists when the blocker is enabled; its absence is not an error.
const blocker = await authFetch("/api/blocker/status").catch(() => null)
if (blocker?.ok) setBlockerStatus((await blocker.json()) as BlockerStatus)
} catch (err) {
setSpamIPsError(err instanceof Error ? err.message : String(err))
} finally {
@@ -701,7 +720,32 @@ export const IssuesVisualizer = ({
}
}
const spamIPSet = useMemo(() => new Set(spamIPs.map((entry) => entry.ip)), [spamIPs])
const detectedIPs = useMemo(() => {
const byIP = new Map<string, DetectedIP>()
for (const entry of spamIPs) byIP.set(entry.ip, { ip: entry.ip, sources: ["honeypot"], count: entry.count })
for (const ip of blockerStatus?.candidates ?? []) {
const known = byIP.get(ip)
if (known) known.sources.push("blocker")
else byIP.set(ip, { ip, sources: ["blocker"] })
}
return Array.from(byIP.values())
}, [spamIPs, blockerStatus])
const spamIPSet = useMemo(() => new Set(detectedIPs.map((entry) => entry.ip)), [detectedIPs])
const crawlersHidden = CRAWLER_CLASSES.every((c) => clientExclude.has(c))
const toggleCrawlers = () => {
setClientExclude((prev) => {
const next = new Set(prev)
if (CRAWLER_CLASSES.every((c) => prev.has(c))) CRAWLER_CLASSES.forEach((c) => next.delete(c))
else CRAWLER_CLASSES.forEach((c) => next.add(c))
return next
})
setClientInclude((prev) => {
const next = new Set(prev)
CRAWLER_CLASSES.forEach((c) => next.delete(c))
return next
})
}
const statusCounts = useMemo(() => {
const counts: Record<StatusFilter, number> = { all: issues.length, open: 0, resolved: 0, ignored: 0 }
@@ -954,7 +998,7 @@ export const IssuesVisualizer = ({
setExcludeSpamIPs((v) => !v)
}}
aria-pressed={excludeSpamIPs}
title="Hide HTTP errors whose recorded source IP is on the honeypot list"
title="Hide HTTP errors whose recorded source IP is on the honeypot list or the blocker's candidate list"
className={`inline-flex h-9 items-center gap-1.5 rounded-[9px] border px-2.5 text-[12px] font-medium transition-colors ${
excludeSpamIPs
? "border-ds-warn/40 bg-ds-warn-bg text-ds-warn"
@@ -964,6 +1008,20 @@ export const IssuesVisualizer = ({
<ShieldBan className="h-3.5 w-3.5" aria-hidden="true" />
{excludeSpamIPs ? "Spam IPs hidden" : "Hide spam IPs"}
</button>
<button
type="button"
onClick={toggleCrawlers}
aria-pressed={crawlersHidden}
title="Hide issues whose requests came from AI crawlers, AI agents, search/SEO crawlers, scan tools or other self-declared bots (by User-Agent)"
className={`inline-flex h-9 items-center gap-1.5 rounded-[9px] border px-2.5 text-[12px] font-medium transition-colors ${
crawlersHidden
? "border-ds-warn/40 bg-ds-warn-bg text-ds-warn"
: "border-ds-line-strong text-ds-text-3 hover:text-ds-text-2"
}`}
>
<Bot className="h-3.5 w-3.5" aria-hidden="true" />
{crawlersHidden ? "Crawlers hidden" : "Hide crawlers"}
</button>
<button
type="button"
onClick={() => setNeedleModalOpen(true)}
@@ -1030,8 +1088,11 @@ export const IssuesVisualizer = ({
<div className="rounded-[12px] border border-ds-warn/30 bg-ds-warn-bg p-3 text-[12.5px] text-ds-text-2">
<div className="mb-2 flex items-center justify-between gap-3">
<div>
<div className="font-bold text-ds-warn">Promoted honeypot IPs</div>
<div className="text-[11px] text-ds-text-3">Used by the ingress tarpit synchronizer.</div>
<div className="font-bold text-ds-warn">Detected scanner IPs</div>
<div className="text-[11px] text-ds-text-3">
Honeypot-promoted IPs (ingress tarpit) and blocker candidates
{blockerStatus ? (blockerStatus.dryRun ? " (blocker in dry-run: nothing is blocked)" : " (blocker enforcing)") : ""}.
</div>
</div>
<button
type="button"
@@ -1044,15 +1105,18 @@ export const IssuesVisualizer = ({
</div>
{spamIPsLoading && <div className="text-ds-text-3">Loading…</div>}
{spamIPsError && <div className="text-ds-critical">Failed to load: {spamIPsError}</div>}
{!spamIPsLoading && !spamIPsError && spamIPs.length === 0 && (
<div className="text-ds-text-3">No promoted IPs.</div>
{!spamIPsLoading && !spamIPsError && detectedIPs.length === 0 && (
<div className="text-ds-text-3">No detected IPs.</div>
)}
{spamIPs.length > 0 && (
{detectedIPs.length > 0 && (
<div className="grid gap-1 sm:grid-cols-2 lg:grid-cols-3">
{spamIPs.map((entry) => (
<div key={entry.ip} className="flex items-center justify-between rounded-[8px] bg-ds-bg px-2.5 py-1.5 font-[family-name:var(--font-data)]">
<span className="text-ds-text">{entry.ip}</span>
<span className="text-[10.5px] text-ds-warn">×{entry.count}</span>
{detectedIPs.map((entry) => (
<div key={entry.ip} className="flex items-center justify-between gap-2 rounded-[8px] bg-ds-bg px-2.5 py-1.5 font-[family-name:var(--font-data)]">
<IpButton ip={entry.ip} />
<span className="shrink-0 whitespace-nowrap text-[10.5px] text-ds-warn">
{entry.sources.join(" + ")}
{entry.count !== undefined ? ` ×${entry.count}` : ""}
</span>
</div>
))}
</div>
@@ -1069,7 +1133,7 @@ export const IssuesVisualizer = ({
}}
className="self-start text-[11.5px] text-ds-text-3 hover:text-ds-text-2"
>
View promoted spam IPs ({spamIPs.length})
View detected scanner IPs ({detectedIPs.length})
</button>
)}