Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Score crawl inputs by SSRF-prone field names and register ping-back canaries for operator paste confirmation.
292 lines
7.8 KiB
Go
292 lines
7.8 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/google/uuid"
|
|
|
|
dbpkg "crypto-miner-server/internal/db"
|
|
"crypto-miner-server/internal/recon"
|
|
)
|
|
|
|
type ReconHandler struct {
|
|
db *dbpkg.Database
|
|
wsHub *WSHub
|
|
publicURL func() string
|
|
canaryHub *ReconCanaryHub
|
|
}
|
|
|
|
func NewReconHandler(database *dbpkg.Database, hub *WSHub, publicURL ...func() string) *ReconHandler {
|
|
var fn func() string
|
|
if len(publicURL) > 0 {
|
|
fn = publicURL[0]
|
|
}
|
|
return &ReconHandler{db: database, wsHub: hub, publicURL: fn, canaryHub: NewReconCanaryHub()}
|
|
}
|
|
|
|
func (h *ReconHandler) Scan(w http.ResponseWriter, r *http.Request) {
|
|
var req recon.ScanRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.Host = strings.TrimSpace(req.Host)
|
|
if req.Host == "" {
|
|
http.Error(w, "host required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
host, err := recon.NormalizeHost(req.Host)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
req.Host = host
|
|
scanID := uuid.New().String()
|
|
if req.SSRFCanary {
|
|
h.registerSSRfCanary(scanID, host, r)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusAccepted)
|
|
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
|
"scan_id": scanID,
|
|
"status": "running",
|
|
})
|
|
|
|
go h.runScan(req, scanID, AuthUsername(r))
|
|
}
|
|
|
|
func (h *ReconHandler) runScan(req recon.ScanRequest, scanID, operator string) {
|
|
emit := func(eventType string, payload map[string]interface{}) {
|
|
if h != nil && h.wsHub != nil {
|
|
h.wsHub.broadcastDashboard(Message{Type: eventType, Payload: mustMarshal(payload)})
|
|
}
|
|
}
|
|
|
|
report, err := recon.ScanStream(req, scanID, emit)
|
|
if err != nil {
|
|
if h != nil && h.wsHub != nil {
|
|
h.wsHub.broadcastDashboard(Message{
|
|
Type: "recon_complete",
|
|
Payload: mustMarshal(map[string]interface{}{"scan_id": scanID, "host": req.Host, "status": "failed", "error": err.Error()}),
|
|
})
|
|
}
|
|
return
|
|
}
|
|
report.ScanID = scanID
|
|
if report.Status == "" {
|
|
report.Status = "complete"
|
|
}
|
|
if req.SSRFCanary {
|
|
h.finalizeSSRfCanary(scanID, report)
|
|
}
|
|
if h != nil && h.db != nil {
|
|
_ = h.db.InsertReconScan(report)
|
|
}
|
|
|
|
if h != nil && h.wsHub != nil {
|
|
h.wsHub.broadcastDashboard(Message{
|
|
Type: "recon_complete",
|
|
Payload: mustMarshal(map[string]interface{}{
|
|
"scan_id": scanID, "host": report.Host, "status": report.Status, "profile": report.Profile,
|
|
}),
|
|
})
|
|
}
|
|
emitter := &HubSeerEmitter{Hub: oathHub(h), DB: h.db}
|
|
_ = emitter.EmitSeerEvent("recon_complete", "", map[string]interface{}{
|
|
"scan_id": scanID, "host": report.Host, "profile": report.Profile, "open_ports": openPortList(report.Ports),
|
|
})
|
|
|
|
if h != nil && h.db != nil {
|
|
_ = (&OathLedgerBridge{DB: h.db, Hub: oathHub(h)}).Record(
|
|
operator, dbpkg.OathReconScan, "", "", dbpkg.OathOutcomeSuccess,
|
|
map[string]string{"host": report.Host, "scan_id": scanID},
|
|
map[string]interface{}{
|
|
"host": report.Host, "scan_id": scanID,
|
|
"open_ports": openPortList(report.Ports), "ssrf_score": crawlSSRFScore(report.Crawl),
|
|
"recommendations": len(report.Recommendations), "cms_fingerprints": crawlCMS(report.Crawl),
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
// GET /api/v1/recon/history?host=
|
|
func (h *ReconHandler) History(w http.ResponseWriter, r *http.Request) {
|
|
host := strings.TrimSpace(r.URL.Query().Get("host"))
|
|
if host == "" {
|
|
http.Error(w, "host required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
nhost, err := recon.NormalizeHost(host)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if h == nil || h.db == nil {
|
|
writeJSON(w, map[string]interface{}{"host": nhost, "history": []recon.ReconHistoryEntry{}})
|
|
return
|
|
}
|
|
rows, err := h.db.ListReconScansByHost(nhost, 10)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
writeJSON(w, map[string]interface{}{"host": nhost, "history": recon.BuildHistory(rows)})
|
|
}
|
|
|
|
// GET /api/v1/recon/export/{scan_id}?format=json|pdf
|
|
func (h *ReconHandler) Export(w http.ResponseWriter, r *http.Request) {
|
|
scanID := strings.TrimSpace(chi.URLParam(r, "scan_id"))
|
|
if scanID == "" {
|
|
http.Error(w, "scan_id required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
format := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("format")))
|
|
if format == "" {
|
|
format = "json"
|
|
}
|
|
if h == nil || h.db == nil {
|
|
http.Error(w, "database unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
report, err := h.db.GetReconScan(scanID)
|
|
if err != nil {
|
|
http.Error(w, "scan not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
switch format {
|
|
case "json":
|
|
writeJSON(w, report)
|
|
case "pdf":
|
|
w.Header().Set("Content-Type", "application/pdf")
|
|
w.Header().Set("Content-Disposition", `attachment; filename="recon-`+scanID+`.pdf"`)
|
|
_, _ = w.Write(recon.ReportToPDF(report))
|
|
default:
|
|
http.Error(w, "format must be json or pdf", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
func (h *ReconHandler) registerSSRfCanary(scanID, host string, r *http.Request) {
|
|
base := ""
|
|
if h.publicURL != nil {
|
|
base = strings.TrimSpace(h.publicURL())
|
|
}
|
|
if base == "" && r != nil {
|
|
hh := strings.TrimSpace(r.Header.Get("X-Forwarded-Host"))
|
|
if hh == "" {
|
|
hh = r.Host
|
|
}
|
|
proto := strings.TrimSpace(r.Header.Get("X-Forwarded-Proto"))
|
|
if proto == "" {
|
|
proto = "https"
|
|
}
|
|
if hh != "" {
|
|
base = proto + "://" + hh
|
|
}
|
|
}
|
|
url := recon.BuildSSRfCanaryURL(base, scanID)
|
|
info := &recon.SSRFCanaryInfo{ScanID: scanID, URL: url, Status: "pending", PasteTarget: url}
|
|
if h.db != nil {
|
|
_ = h.db.InsertReconSSRfCanary(scanID, host, url, "", "")
|
|
}
|
|
if h.canaryHub != nil {
|
|
h.canaryHub.Register(scanID, info)
|
|
}
|
|
}
|
|
|
|
func (h *ReconHandler) finalizeSSRfCanary(scanID string, report *recon.ScanReport) {
|
|
info, _ := h.getCanaryInfo(scanID)
|
|
if info == nil || report == nil || report.Crawl == nil {
|
|
return
|
|
}
|
|
recon.ApplyCanaryPasteTarget(report.Crawl.FingerprintFields, info.URL)
|
|
if top := recon.TopFormFieldFingerprint(report.Crawl.FingerprintFields); top != nil {
|
|
info.PasteFieldName, info.PasteFieldID = top.Name, top.ID
|
|
if h.canaryHub != nil {
|
|
h.canaryHub.UpdatePasteField(scanID, top.Name, top.ID)
|
|
}
|
|
if h.db != nil {
|
|
_ = h.db.UpdateReconSSRfCanaryPasteField(scanID, top.Name, top.ID)
|
|
}
|
|
}
|
|
report.Canary = info
|
|
}
|
|
|
|
func (h *ReconHandler) getCanaryInfo(scanID string) (*recon.SSRFCanaryInfo, error) {
|
|
if h.canaryHub != nil {
|
|
if info := h.canaryHub.Get(scanID); info != nil {
|
|
return info, nil
|
|
}
|
|
}
|
|
if h.db != nil {
|
|
return h.db.GetReconSSRfCanary(scanID)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (h *ReconHandler) CanaryStatus(w http.ResponseWriter, r *http.Request) {
|
|
scanID := strings.TrimSpace(chi.URLParam(r, "scan_id"))
|
|
info, err := h.getCanaryInfo(scanID)
|
|
if scanID == "" || err != nil || info == nil {
|
|
http.Error(w, "canary not found", http.StatusNotFound)
|
|
return
|
|
}
|
|
writeJSON(w, info)
|
|
}
|
|
|
|
func (h *ReconHandler) CanaryPing(w http.ResponseWriter, r *http.Request) {
|
|
scanID := strings.TrimSpace(chi.URLParam(r, "scan_id"))
|
|
if scanID == "" {
|
|
writeJSON(w, map[string]interface{}{"ok": false})
|
|
return
|
|
}
|
|
hit := h.canaryHub != nil && h.canaryHub.MarkHit(scanID)
|
|
if !hit && h.db != nil {
|
|
hit, _ = h.db.MarkReconSSRfCanaryHit(scanID)
|
|
}
|
|
if !hit {
|
|
writeJSON(w, map[string]interface{}{"ok": false, "error": "canary not found"})
|
|
return
|
|
}
|
|
log.Printf("[recon] SSRF canary hit scan_id=%s", scanID)
|
|
if h.wsHub != nil {
|
|
BroadcastReconCanaryHit(h.wsHub, scanID)
|
|
}
|
|
writeJSON(w, map[string]interface{}{"ok": true, "scan_id": scanID, "status": "confirmed"})
|
|
}
|
|
|
|
func oathHub(h *ReconHandler) *WSHub {
|
|
if h == nil {
|
|
return nil
|
|
}
|
|
return h.wsHub
|
|
}
|
|
|
|
func openPortList(ports []recon.PortResult) []int {
|
|
var out []int
|
|
for _, p := range ports {
|
|
if p.Open {
|
|
out = append(out, p.Port)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func crawlSSRFScore(c *recon.CrawlReport) int {
|
|
if c == nil {
|
|
return 0
|
|
}
|
|
return c.SSRFScore
|
|
}
|
|
|
|
func crawlCMS(c *recon.CrawlReport) []string {
|
|
if c == nil {
|
|
return nil
|
|
}
|
|
return c.CMSFingerprints
|
|
}
|