Files
keyhunter/pkg/storage/db.go
salvacybersec aec559d2aa feat(05-01): migrate findings schema with verify_* columns
- schema.sql: new findings columns verified, verify_status, verify_http_code, verify_metadata_json
- db.go: migrateFindingsVerifyColumns runs on Open() for legacy DBs using PRAGMA table_info + ALTER TABLE
- findings.go: Finding struct gains Verified/VerifyStatus/VerifyHTTPCode/VerifyMetadata
- SaveFinding serializes verify metadata as JSON (NULL when nil)
- ListFindings round-trips all verify fields
2026-04-05 15:42:53 +03:00

113 lines
3.0 KiB
Go

package storage
import (
"database/sql"
_ "embed"
"fmt"
_ "modernc.org/sqlite"
)
//go:embed schema.sql
var schemaSQLBytes []byte
// DB wraps the sql.DB connection with KeyHunter-specific behavior.
type DB struct {
sql *sql.DB
}
// Open opens or creates a SQLite database at path, runs embedded schema migrations,
// and enables WAL mode for better concurrent read performance.
// Use ":memory:" for tests.
func Open(path string) (*DB, error) {
sqlDB, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("opening database: %w", err)
}
// Enable WAL mode for concurrent reads
if _, err := sqlDB.Exec("PRAGMA journal_mode=WAL"); err != nil {
sqlDB.Close()
return nil, fmt.Errorf("enabling WAL mode: %w", err)
}
// Enable foreign keys
if _, err := sqlDB.Exec("PRAGMA foreign_keys=ON"); err != nil {
sqlDB.Close()
return nil, fmt.Errorf("enabling foreign keys: %w", err)
}
// Run schema migrations
if _, err := sqlDB.Exec(string(schemaSQLBytes)); err != nil {
sqlDB.Close()
return nil, fmt.Errorf("running schema migrations: %w", err)
}
// Idempotent in-place migration for pre-Phase-5 databases that created
// the findings table before the verify_* columns existed.
if err := migrateFindingsVerifyColumns(sqlDB); err != nil {
sqlDB.Close()
return nil, fmt.Errorf("migrating findings verify columns: %w", err)
}
return &DB{sql: sqlDB}, nil
}
// migrateFindingsVerifyColumns adds the Phase 5 verify_* columns to an
// existing findings table when they are missing. Uses PRAGMA table_info to
// detect the current column set (works on SQLite versions that lack
// ADD COLUMN IF NOT EXISTS).
func migrateFindingsVerifyColumns(sqlDB *sql.DB) error {
rows, err := sqlDB.Query("PRAGMA table_info(findings)")
if err != nil {
return fmt.Errorf("reading findings schema: %w", err)
}
existing := map[string]bool{}
for rows.Next() {
var cid int
var name, ctype string
var notnull, pk int
var dflt sql.NullString
if err := rows.Scan(&cid, &name, &ctype, &notnull, &dflt, &pk); err != nil {
rows.Close()
return fmt.Errorf("scanning findings schema row: %w", err)
}
existing[name] = true
}
if err := rows.Err(); err != nil {
rows.Close()
return err
}
rows.Close()
type colDef struct {
name string
ddl string
}
wanted := []colDef{
{"verified", "ALTER TABLE findings ADD COLUMN verified INTEGER NOT NULL DEFAULT 0"},
{"verify_status", "ALTER TABLE findings ADD COLUMN verify_status TEXT NOT NULL DEFAULT ''"},
{"verify_http_code", "ALTER TABLE findings ADD COLUMN verify_http_code INTEGER NOT NULL DEFAULT 0"},
{"verify_metadata_json", "ALTER TABLE findings ADD COLUMN verify_metadata_json TEXT"},
}
for _, c := range wanted {
if existing[c.name] {
continue
}
if _, err := sqlDB.Exec(c.ddl); err != nil {
return fmt.Errorf("adding column %s: %w", c.name, err)
}
}
return nil
}
// Close closes the underlying database connection.
func (db *DB) Close() error {
return db.sql.Close()
}
// SQL returns the underlying sql.DB for advanced use cases.
func (db *DB) SQL() *sql.DB {
return db.sql
}