mirror of
https://github.com/projectdiscovery/httpx
synced 2026-06-08 16:50:17 +00:00
267 lines
6.5 KiB
Go
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
|
|
}
|