Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-05-12 19:12:29 +02:00
commit e8e64eff12
101 changed files with 10081 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
package db
import (
"database/sql"
_ "modernc.org/sqlite"
)
type DB struct {
conn *sql.DB
}
func Open(path string) (*DB, error) {
conn, err := sql.Open("sqlite", path)
if err != nil {
return nil, err
}
d := &DB{conn: conn}
if err := d.migrate(); err != nil {
conn.Close()
return nil, err
}
return d, nil
}
func (d *DB) migrate() error {
_, err := d.conn.Exec(`
CREATE TABLE IF NOT EXISTS entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL,
method TEXT NOT NULL,
host TEXT NOT NULL,
path TEXT NOT NULL,
status_code INTEGER NOT NULL,
request_raw TEXT NOT NULL,
response_raw TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS scope (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL CHECK(kind IN ('whitelist','blacklist')),
pattern TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS replay_entries (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp DATETIME NOT NULL,
scheme TEXT NOT NULL,
host TEXT NOT NULL,
path TEXT NOT NULL,
method TEXT NOT NULL,
original_raw TEXT NOT NULL,
request_raw TEXT NOT NULL,
response_raw TEXT NOT NULL,
status_code INTEGER NOT NULL,
error_msg TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS plugins (
name TEXT PRIMARY KEY,
enabled INTEGER NOT NULL DEFAULT 1,
config_text TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS findings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
plugin_name TEXT NOT NULL,
dedup_key TEXT NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
severity TEXT NOT NULL DEFAULT 'info',
dismissed INTEGER NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL,
UNIQUE(plugin_name, dedup_key)
);
INSERT INTO scope (kind, pattern)
SELECT 'blacklist', '\.(js|css|png|gif|ico|woff2?|ttf|svg)(\?.*)?$'
WHERE NOT EXISTS (SELECT 1 FROM scope);
`)
return err
}
func (d *DB) Close() error {
if d == nil {
return nil
}
return d.conn.Close()
}
// CountEntriesAt opens the database at path read-only, counts entries, and
// closes it immediately. Safe to call on files not yet opened by the app.
func CountEntriesAt(path string) int {
conn, err := sql.Open("sqlite", path)
if err != nil {
return 0
}
defer conn.Close()
var n int
conn.QueryRow(`SELECT COUNT(*) FROM entries`).Scan(&n)
return n
}
+131
View File
@@ -0,0 +1,131 @@
package db
import (
"database/sql"
"fmt"
"strings"
"time"
)
type Entry struct {
ID int64
Timestamp time.Time
Method string
Host string
Path string
StatusCode int
RequestRaw string
ResponseRaw string
}
// HasDuplicate returns true if an entry with the same method, host, path and
// request body already exists. Used to implement skip_duplicates filtering.
func (d *DB) HasDuplicate(method, host, path, body string) (bool, error) {
rows, err := d.conn.Query(
`SELECT request_raw FROM entries WHERE method = ? AND host = ? AND path = ?`,
method, host, path,
)
if err != nil {
return false, err
}
defer rows.Close()
for rows.Next() {
var raw string
if err := rows.Scan(&raw); err != nil {
return false, err
}
parts := strings.SplitN(raw, "\n\n", 2)
entryBody := ""
if len(parts) == 2 {
entryBody = parts[1]
}
if entryBody == body {
return true, nil
}
}
return false, rows.Err()
}
func (d *DB) InsertEntry(e Entry) (Entry, error) {
res, err := d.conn.Exec(
`INSERT INTO entries (timestamp, method, host, path, status_code, request_raw, response_raw)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
e.Timestamp.UTC().Format(time.RFC3339),
e.Method, e.Host, e.Path, e.StatusCode, e.RequestRaw, e.ResponseRaw,
)
if err != nil {
return e, err
}
e.ID, _ = res.LastInsertId()
return e, nil
}
func scanEntries(rows *sql.Rows) ([]Entry, error) {
var entries []Entry
for rows.Next() {
var e Entry
var ts string
if err := rows.Scan(&e.ID, &ts, &e.Method, &e.Host, &e.Path, &e.StatusCode, &e.RequestRaw, &e.ResponseRaw); err != nil {
return nil, err
}
e.Timestamp, _ = time.Parse(time.RFC3339, ts)
entries = append(entries, e)
}
return entries, rows.Err()
}
func (d *DB) ListEntries() ([]Entry, error) {
rows, err := d.conn.Query(
`SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw
FROM entries ORDER BY id DESC`,
)
if err != nil {
return nil, err
}
defer rows.Close()
return scanEntries(rows)
}
func (d *DB) SearchEntries(term string) ([]Entry, error) {
like := "%" + term + "%"
rows, err := d.conn.Query(
`SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw
FROM entries
WHERE method LIKE ? OR host LIKE ? OR path LIKE ? OR request_raw LIKE ? OR response_raw LIKE ?
ORDER BY id DESC`,
like, like, like, like, like,
)
if err != nil {
return nil, err
}
defer rows.Close()
return scanEntries(rows)
}
// QueryEntries executes a user-supplied query against the entries table.
// If the query does not start with SELECT, it is treated as a WHERE expression
// and wrapped automatically (e.g. "status_code = 404" becomes a full SELECT).
func (d *DB) QueryEntries(rawSQL string) ([]Entry, error) {
q := strings.TrimSpace(rawSQL)
if !strings.HasPrefix(strings.ToUpper(q), "SELECT") {
q = "SELECT id, timestamp, method, host, path, status_code, request_raw, response_raw FROM entries WHERE " + q
} else if strings.ContainsAny(strings.ToUpper(q), "INSERTDELETEUPDATEDROP") {
return nil, fmt.Errorf("only SELECT queries are allowed")
}
rows, err := d.conn.Query(q)
if err != nil {
return nil, err
}
defer rows.Close()
return scanEntries(rows)
}
func (d *DB) DeleteEntry(id int64) error {
_, err := d.conn.Exec(`DELETE FROM entries WHERE id = ?`, id)
return err
}
func (d *DB) DeleteAllEntries() error {
_, err := d.conn.Exec(`DELETE FROM entries`)
return err
}
+63
View File
@@ -0,0 +1,63 @@
package db
import "time"
var findingTimeFormats = []string{time.RFC3339, "2006-01-02 15:04:05"}
type Finding struct {
ID int64
PluginName string
DedupKey string
Title string
Description string
Severity string
CreatedAt time.Time
}
// UpsertFinding inserts the finding if the (plugin_name, dedup_key) pair does
// not already exist. Returns true when the row was actually inserted.
func (d *DB) UpsertFinding(f Finding) (bool, error) {
res, err := d.conn.Exec(
`INSERT OR IGNORE INTO findings (plugin_name, dedup_key, title, description, severity, dismissed, created_at)
VALUES (?, ?, ?, ?, ?, 0, ?)`,
f.PluginName, f.DedupKey, f.Title, f.Description, f.Severity,
f.CreatedAt.UTC().Format(time.RFC3339),
)
if err != nil {
return false, err
}
n, _ := res.RowsAffected()
return n > 0, nil
}
func (d *DB) LoadFindings() ([]Finding, error) {
rows, err := d.conn.Query(
`SELECT id, plugin_name, dedup_key, title, description, severity, created_at
FROM findings WHERE dismissed = 0 ORDER BY id DESC`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var out []Finding
for rows.Next() {
var f Finding
var ts string
if err := rows.Scan(&f.ID, &f.PluginName, &f.DedupKey, &f.Title, &f.Description, &f.Severity, &ts); err != nil {
return nil, err
}
for _, layout := range findingTimeFormats {
if t, err := time.Parse(layout, ts); err == nil {
f.CreatedAt = t
break
}
}
out = append(out, f)
}
return out, rows.Err()
}
func (d *DB) DismissFinding(id int64) error {
_, err := d.conn.Exec(`UPDATE findings SET dismissed = 1 WHERE id = ?`, id)
return err
}
+35
View File
@@ -0,0 +1,35 @@
package db
type PluginState struct {
Name string
Enabled bool
ConfigText string
}
func (d *DB) SavePluginState(name string, enabled bool, configText string) error {
_, err := d.conn.Exec(
`INSERT INTO plugins (name, enabled, config_text) VALUES (?, ?, ?)
ON CONFLICT(name) DO UPDATE SET enabled = excluded.enabled, config_text = excluded.config_text`,
name, enabled, configText,
)
return err
}
func (d *DB) LoadPluginStates() ([]PluginState, error) {
rows, err := d.conn.Query(`SELECT name, enabled, config_text FROM plugins`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []PluginState
for rows.Next() {
var s PluginState
var enabled int
if err := rows.Scan(&s.Name, &enabled, &s.ConfigText); err != nil {
return nil, err
}
s.Enabled = enabled != 0
out = append(out, s)
}
return out, rows.Err()
}
+76
View File
@@ -0,0 +1,76 @@
package db
import (
"time"
)
type ReplayEntry struct {
ID int64
Timestamp time.Time
Scheme string
Host string
Path string
Method string
OriginalRaw string
RequestRaw string
ResponseRaw string
StatusCode int
ErrorMsg string
}
func (d *DB) InsertReplayEntry(e ReplayEntry) (int64, error) {
res, err := d.conn.Exec(
`INSERT INTO replay_entries (timestamp, scheme, host, path, method, original_raw, request_raw, response_raw, status_code, error_msg)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
e.Timestamp.UTC().Format(time.RFC3339),
e.Scheme, e.Host, e.Path, e.Method,
e.OriginalRaw, e.RequestRaw, e.ResponseRaw,
e.StatusCode, e.ErrorMsg,
)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
func (d *DB) UpdateReplayEntry(e ReplayEntry) error {
_, err := d.conn.Exec(
`UPDATE replay_entries SET request_raw=?, response_raw=?, status_code=?, error_msg=? WHERE id=?`,
e.RequestRaw, e.ResponseRaw, e.StatusCode, e.ErrorMsg, e.ID,
)
return err
}
func (d *DB) ListReplayEntries() ([]ReplayEntry, error) {
rows, err := d.conn.Query(
`SELECT id, timestamp, scheme, host, path, method, original_raw, request_raw, response_raw, status_code, error_msg
FROM replay_entries ORDER BY id ASC`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var entries []ReplayEntry
for rows.Next() {
var e ReplayEntry
var ts string
if err := rows.Scan(&e.ID, &ts, &e.Scheme, &e.Host, &e.Path, &e.Method,
&e.OriginalRaw, &e.RequestRaw, &e.ResponseRaw, &e.StatusCode, &e.ErrorMsg); err != nil {
return nil, err
}
e.Timestamp, _ = time.Parse(time.RFC3339, ts)
entries = append(entries, e)
}
return entries, rows.Err()
}
func (d *DB) DeleteReplayEntry(id int64) error {
_, err := d.conn.Exec(`DELETE FROM replay_entries WHERE id = ?`, id)
return err
}
func (d *DB) DeleteAllReplayEntries() error {
_, err := d.conn.Exec(`DELETE FROM replay_entries`)
return err
}
+45
View File
@@ -0,0 +1,45 @@
package db
func (d *DB) SaveScope(whitelist, blacklist []string) error {
tx, err := d.conn.Begin()
if err != nil {
return err
}
if _, err := tx.Exec(`DELETE FROM scope`); err != nil {
tx.Rollback()
return err
}
for _, p := range whitelist {
if _, err := tx.Exec(`INSERT INTO scope (kind, pattern) VALUES ('whitelist', ?)`, p); err != nil {
tx.Rollback()
return err
}
}
for _, p := range blacklist {
if _, err := tx.Exec(`INSERT INTO scope (kind, pattern) VALUES ('blacklist', ?)`, p); err != nil {
tx.Rollback()
return err
}
}
return tx.Commit()
}
func (d *DB) LoadScope() (whitelist, blacklist []string, err error) {
rows, err := d.conn.Query(`SELECT kind, pattern FROM scope`)
if err != nil {
return nil, nil, err
}
defer rows.Close()
for rows.Next() {
var kind, pattern string
if err := rows.Scan(&kind, &pattern); err != nil {
return nil, nil, err
}
if kind == "whitelist" {
whitelist = append(whitelist, pattern)
} else {
blacklist = append(blacklist, pattern)
}
}
return whitelist, blacklist, rows.Err()
}