mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-10-28 15:51:27 +00:00
P5b: notifier transport registry (webhook/slack/discord/telegram/ gotify/ntfy/smtp via net/smtp), per-monitor+domain alert rules (failsBeforeDown, latencyWarn, certDays, mute). P5c: per-container cpu/mem, optional SMART/ZFS/GPU exec-or-skip collectors, crt.sh subdomain discovery with daily sweep. P5d: opt-in auth (bcrypt, sqlite sessions, 7d rotating cookie, login rate limit, public-route bypass) and multiple boards (boards table, sections.board_id, /api/boards CRUD, slug routing). Authored By: TDvorak <[email protected]> Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
96 lines
2.8 KiB
Go
96 lines
2.8 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/tdvorak/dash/internal/domain"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Subdomain discovery: crt.sh + DNS, persisted per domain and swept at most
|
|
// once a day inside the domain refresh. CT names are untrusted strings —
|
|
// stored as text, rendered as text, never linked.
|
|
|
|
type subdomainView struct {
|
|
Name string `json:"name"`
|
|
IPs []string `json:"ips"`
|
|
FirstSeen string `json:"firstSeen"`
|
|
LastSeen string `json:"lastSeen"`
|
|
}
|
|
|
|
func (s *Server) listSubdomains(c *gin.Context) {
|
|
rows, err := s.db.Query(`SELECT name, ips, first_seen, last_seen
|
|
FROM subdomains WHERE domain_id = ? ORDER BY name`, c.Param("id"))
|
|
if err != nil {
|
|
fail(c, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
out := []subdomainView{}
|
|
for rows.Next() {
|
|
var v subdomainView
|
|
var ips string
|
|
if err := rows.Scan(&v.Name, &ips, &v.FirstSeen, &v.LastSeen); err != nil {
|
|
continue
|
|
}
|
|
_ = json.Unmarshal([]byte(ips), &v.IPs)
|
|
if v.IPs == nil {
|
|
v.IPs = []string{}
|
|
}
|
|
out = append(out, v)
|
|
}
|
|
c.JSON(http.StatusOK, out)
|
|
}
|
|
|
|
// refreshSubdomains forces a discovery run for one domain.
|
|
func (s *Server) refreshSubdomains(c *gin.Context) {
|
|
id := c.Param("id")
|
|
d, err := s.loadDomain(id)
|
|
if notFound(err) {
|
|
fail(c, http.StatusNotFound, "domain not found")
|
|
return
|
|
}
|
|
s.sweepSubdomains(d)
|
|
s.listSubdomains(c)
|
|
}
|
|
|
|
// sweepSubdomains runs CT+DNS discovery and upserts rows. Called from
|
|
// refreshDomain when sub_checked_at is stale (>24h), and on manual refresh.
|
|
func (s *Server) sweepSubdomains(d *domainRow) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
found, err := domain.DiscoverSubdomains(ctx, d.Name)
|
|
if err != nil {
|
|
s.log.Warn("subdomain discovery", zap.String("domain", d.Name), zap.Error(err))
|
|
return
|
|
}
|
|
for _, f := range found {
|
|
ips, _ := json.Marshal(f.IPs)
|
|
if _, err := s.db.Exec(`INSERT INTO subdomains (id, domain_id, name, ips)
|
|
VALUES (?,?,?,?)
|
|
ON CONFLICT(domain_id, name) DO UPDATE SET ips = excluded.ips,
|
|
last_seen = strftime('%Y-%m-%dT%H:%M:%fZ','now')`,
|
|
newID("sub"), d.ID, f.Name, string(ips)); err != nil {
|
|
s.log.Error("subdomain upsert", zap.Error(err))
|
|
}
|
|
}
|
|
_, _ = s.db.Exec(`UPDATE domains SET sub_checked_at = strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id = ?`, d.ID)
|
|
}
|
|
|
|
// maybeSweepSubdomains gates discovery to once per day per domain.
|
|
func (s *Server) maybeSweepSubdomains(d *domainRow) {
|
|
var checked *string
|
|
_ = s.db.QueryRow(`SELECT sub_checked_at FROM domains WHERE id = ?`, d.ID).Scan(&checked)
|
|
if checked != nil && *checked != "" {
|
|
if t, err := time.Parse("2006-01-02T15:04:05.000Z", *checked); err == nil &&
|
|
time.Since(t) < 24*time.Hour {
|
|
return
|
|
}
|
|
}
|
|
s.sweepSubdomains(d)
|
|
}
|