Files
2026-03-04 12:35:05 +01:00

267 lines
6.5 KiB
Go

package db
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/lib/pq"
"github.com/projectdiscovery/httpx/runner"
)
func init() {
Register(PostgreSQL, newPostgresDatabase)
}
type postgresDatabase struct {
cfg *Config
db *sql.DB
}
func newPostgresDatabase(cfg *Config) (Database, error) {
return &postgresDatabase{cfg: cfg}, nil
}
func (p *postgresDatabase) Connect(ctx context.Context) error {
db, err := sql.Open("postgres", p.cfg.ConnectionString)
if err != nil {
return fmt.Errorf("failed to open PostgreSQL connection: %w", err)
}
if err := db.PingContext(ctx); err != nil {
return fmt.Errorf("failed to ping PostgreSQL: %w", err)
}
p.db = db
return nil
}
func (p *postgresDatabase) Close() error {
if p.db != nil {
return p.db.Close()
}
return nil
}
func (p *postgresDatabase) EnsureSchema(ctx context.Context) error {
tableName := pq.QuoteIdentifier(p.cfg.TableName)
idxTimestamp := pq.QuoteIdentifier("idx_" + p.cfg.TableName + "_timestamp")
idxURL := pq.QuoteIdentifier("idx_" + p.cfg.TableName + "_url")
idxHost := pq.QuoteIdentifier("idx_" + p.cfg.TableName + "_host")
idxStatusCode := pq.QuoteIdentifier("idx_" + p.cfg.TableName + "_status_code")
idxTech := pq.QuoteIdentifier("idx_" + p.cfg.TableName + "_tech")
schema := fmt.Sprintf(`
CREATE TABLE IF NOT EXISTS %s (
id BIGSERIAL PRIMARY KEY,
-- Basic info
timestamp TIMESTAMP WITH TIME ZONE,
url TEXT,
input TEXT,
host TEXT,
port TEXT,
scheme TEXT,
path TEXT,
method TEXT,
final_url TEXT,
-- Response data
status_code INTEGER,
content_length INTEGER,
content_type TEXT,
title TEXT,
webserver TEXT,
response_time TEXT,
location TEXT,
body TEXT,
body_preview TEXT,
raw_header TEXT,
request TEXT,
-- Network info
host_ip TEXT,
a TEXT[],
aaaa TEXT[],
cname TEXT[],
resolvers TEXT[],
body_fqdn TEXT[],
body_domains TEXT[],
sni TEXT,
-- Technology detection
tech TEXT[],
-- Hashes and fingerprints
hash JSONB,
favicon TEXT,
favicon_md5 TEXT,
favicon_path TEXT,
favicon_url TEXT,
jarm_hash TEXT,
-- CDN info
cdn BOOLEAN,
cdn_name TEXT,
cdn_type TEXT,
-- ASN info
asn JSONB,
-- TLS data
tls JSONB,
-- CSP data
csp JSONB,
-- Status flags
failed BOOLEAN,
error TEXT,
websocket BOOLEAN,
http2 BOOLEAN,
pipeline BOOLEAN,
vhost BOOLEAN,
-- Metrics
words INTEGER,
lines INTEGER,
-- Headers and extracts
header JSONB,
extracts JSONB,
extract_regex TEXT[],
-- Chain data
chain JSONB,
chain_status_codes INTEGER[],
-- Headless/Screenshot
headless_body TEXT,
screenshot_bytes BYTEA,
screenshot_path TEXT,
screenshot_path_rel TEXT,
stored_response_path TEXT,
-- Knowledge base
knowledgebase JSONB,
-- Link requests
link_request JSONB,
-- Trace
trace JSONB
);
CREATE INDEX IF NOT EXISTS %s ON %s(timestamp DESC);
CREATE INDEX IF NOT EXISTS %s ON %s(url);
CREATE INDEX IF NOT EXISTS %s ON %s(host);
CREATE INDEX IF NOT EXISTS %s ON %s(status_code);
CREATE INDEX IF NOT EXISTS %s ON %s USING GIN(tech);
`,
tableName,
idxTimestamp, tableName,
idxURL, tableName,
idxHost, tableName,
idxStatusCode, tableName,
idxTech, tableName,
)
_, err := p.db.ExecContext(ctx, schema)
if err != nil {
return fmt.Errorf("failed to create schema: %w", err)
}
return nil
}
func (p *postgresDatabase) InsertBatch(ctx context.Context, results []runner.Result) error {
if len(results) == 0 {
return nil
}
tx, err := p.db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer func() {
_ = tx.Rollback()
}()
tableName := pq.QuoteIdentifier(p.cfg.TableName)
query := fmt.Sprintf(`
INSERT INTO %s (
timestamp, url, input, host, port, scheme, path, method, final_url,
status_code, content_length, content_type, title, webserver, response_time,
location, body, body_preview, raw_header, request,
host_ip, a, aaaa, cname, resolvers, body_fqdn, body_domains, sni,
tech, hash, favicon, favicon_md5, favicon_path, favicon_url, jarm_hash,
cdn, cdn_name, cdn_type, asn, tls, csp,
failed, error, websocket, http2, pipeline, vhost,
words, lines, header, extracts, extract_regex,
chain, chain_status_codes,
headless_body, screenshot_bytes, screenshot_path, screenshot_path_rel, stored_response_path,
knowledgebase, link_request, trace
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9,
$10, $11, $12, $13, $14, $15,
$16, $17, $18, $19, $20,
$21, $22, $23, $24, $25, $26, $27, $28,
$29, $30, $31, $32, $33, $34, $35,
$36, $37, $38, $39, $40, $41,
$42, $43, $44, $45, $46, $47,
$48, $49, $50, $51, $52,
$53, $54,
$55, $56, $57, $58, $59,
$60, $61, $62
)`, tableName)
stmt, err := tx.PrepareContext(ctx, query)
if err != nil {
return fmt.Errorf("failed to prepare statement: %w", err)
}
defer func() {
_ = stmt.Close()
}()
for _, r := range results {
hashJSON, _ := json.Marshal(r.Hashes)
asnJSON, _ := json.Marshal(r.ASN)
tlsJSON, _ := json.Marshal(r.TLSData)
cspJSON, _ := json.Marshal(r.CSPData)
headerJSON, _ := json.Marshal(r.ResponseHeaders)
extractsJSON, _ := json.Marshal(r.Extracts)
chainJSON, _ := json.Marshal(r.Chain)
kbJSON, _ := json.Marshal(r.KnowledgeBase)
linkReqJSON, _ := json.Marshal(r.LinkRequest)
traceJSON, _ := json.Marshal(r.Trace)
_, err = stmt.ExecContext(ctx,
r.Timestamp, r.URL, r.Input, r.Host, r.Port, r.Scheme, r.Path, r.Method, r.FinalURL,
r.StatusCode, r.ContentLength, r.ContentType, r.Title, r.WebServer, r.ResponseTime,
r.Location, r.ResponseBody, r.BodyPreview, r.RawHeaders, r.Request,
r.HostIP, pq.Array(r.A), pq.Array(r.AAAA), pq.Array(r.CNAMEs), pq.Array(r.Resolvers), pq.Array(r.Fqdns), pq.Array(r.Domains), r.SNI,
pq.Array(r.Technologies), hashJSON, r.FavIconMMH3, r.FavIconMD5, r.FaviconPath, r.FaviconURL, r.JarmHash,
r.CDN, r.CDNName, r.CDNType, asnJSON, tlsJSON, cspJSON,
r.Failed, r.Error, r.WebSocket, r.HTTP2, r.Pipeline, r.VHost,
r.Words, r.Lines, headerJSON, extractsJSON, pq.Array(r.ExtractRegex),
chainJSON, pq.Array(r.ChainStatusCodes),
r.HeadlessBody, r.ScreenshotBytes, r.ScreenshotPath, r.ScreenshotPathRel, r.StoredResponsePath,
kbJSON, linkReqJSON, traceJSON,
)
if err != nil {
return fmt.Errorf("failed to insert result: %w", err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}
func (p *postgresDatabase) Type() DatabaseType {
return PostgreSQL
}