merge: plan 01-03 storage layer
This commit is contained in:
@@ -48,7 +48,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
|||||||
Plans:
|
Plans:
|
||||||
- [x] 01-01-PLAN.md — Go module init, dependency installation, test scaffolding and testdata fixtures
|
- [x] 01-01-PLAN.md — Go module init, dependency installation, test scaffolding and testdata fixtures
|
||||||
- [ ] 01-02-PLAN.md — Provider registry: YAML schema, embed loader, Aho-Corasick automaton, Registry struct
|
- [ ] 01-02-PLAN.md — Provider registry: YAML schema, embed loader, Aho-Corasick automaton, Registry struct
|
||||||
- [ ] 01-03-PLAN.md — Storage layer: AES-256-GCM encryption, Argon2id key derivation, SQLite + Finding CRUD
|
- [x] 01-03-PLAN.md — Storage layer: AES-256-GCM encryption, Argon2id key derivation, SQLite + Finding CRUD
|
||||||
- [ ] 01-04-PLAN.md — Scan engine pipeline: keyword pre-filter, regex+entropy detector, FileSource, ants worker pool
|
- [ ] 01-04-PLAN.md — Scan engine pipeline: keyword pre-filter, regex+entropy detector, FileSource, ants worker pool
|
||||||
- [ ] 01-05-PLAN.md — CLI wiring: scan, providers list/info/stats, config init/set/get, output table
|
- [ ] 01-05-PLAN.md — CLI wiring: scan, providers list/info/stats, config init/set/get, output table
|
||||||
|
|
||||||
@@ -255,7 +255,7 @@ Phases execute in numeric order: 1 → 2 → 3 → ... → 18
|
|||||||
|
|
||||||
| Phase | Plans Complete | Status | Completed |
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Foundation | 0/5 | Planning complete | - |
|
| 1. Foundation | 1/5 | In Progress| |
|
||||||
| 2. Tier 1-2 Providers | 0/? | Not started | - |
|
| 2. Tier 1-2 Providers | 0/? | Not started | - |
|
||||||
| 3. Tier 3-9 Providers | 0/? | Not started | - |
|
| 3. Tier 3-9 Providers | 0/? | Not started | - |
|
||||||
| 4. Input Sources | 0/? | Not started | - |
|
| 4. Input Sources | 0/? | Not started | - |
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
gsd_state_version: 1.0
|
gsd_state_version: 1.0
|
||||||
milestone: v1.0
|
milestone: v1.0
|
||||||
milestone_name: milestone
|
milestone_name: milestone
|
||||||
status: executing
|
status: planning
|
||||||
stopped_at: Completed 01-01-PLAN.md — Go module initialized, all Phase 1 deps pinned, test scaffolding created
|
stopped_at: Completed 01-foundation-03-PLAN.md
|
||||||
last_updated: "2026-04-04T21:06:08.660Z"
|
last_updated: "2026-04-04T21:07:04.658Z"
|
||||||
last_activity: 2026-04-04
|
last_activity: 2026-04-04 — Roadmap created, 18 phases defined covering 146 v1 requirements
|
||||||
progress:
|
progress:
|
||||||
total_phases: 18
|
total_phases: 18
|
||||||
completed_phases: 0
|
completed_phases: 0
|
||||||
total_plans: 5
|
total_plans: 5
|
||||||
completed_plans: 1
|
completed_plans: 1
|
||||||
percent: 0
|
percent: 20
|
||||||
---
|
---
|
||||||
|
|
||||||
# Project State
|
# Project State
|
||||||
@@ -21,16 +21,16 @@ progress:
|
|||||||
See: .planning/PROJECT.md (updated 2026-04-04)
|
See: .planning/PROJECT.md (updated 2026-04-04)
|
||||||
|
|
||||||
**Core value:** Detect leaked LLM API keys across more providers and more internet sources than any other tool, with active verification to confirm keys are real and alive.
|
**Core value:** Detect leaked LLM API keys across more providers and more internet sources than any other tool, with active verification to confirm keys are real and alive.
|
||||||
**Current focus:** Phase 01 — Foundation
|
**Current focus:** Phase 1 — Foundation
|
||||||
|
|
||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Phase: 01 (Foundation) — EXECUTING
|
Phase: 1 of 18 (Foundation)
|
||||||
Plan: 2 of 5
|
Plan: 0 of ? in current phase
|
||||||
Status: Ready to execute
|
Status: Ready to plan
|
||||||
Last activity: 2026-04-04
|
Last activity: 2026-04-04 — Roadmap created, 18 phases defined covering 146 v1 requirements
|
||||||
|
|
||||||
Progress: [░░░░░░░░░░░░░░░░░░░░] 0%
|
Progress: [██░░░░░░░░] 20%
|
||||||
|
|
||||||
## Performance Metrics
|
## Performance Metrics
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ Progress: [░░░░░░░░░░░░░░░░░░░░] 0%
|
|||||||
- Trend: —
|
- Trend: —
|
||||||
|
|
||||||
*Updated after each plan completion*
|
*Updated after each plan completion*
|
||||||
| Phase 01-foundation P01-01 | 3 | 2 tasks | 15 files |
|
| Phase 01-foundation P03 | 3 | 2 tasks | 7 files |
|
||||||
|
|
||||||
## Accumulated Context
|
## Accumulated Context
|
||||||
|
|
||||||
@@ -65,8 +65,8 @@ Recent decisions affecting current work:
|
|||||||
- Roadmap: Per-source rate limiter architecture (Phase 9) must precede all OSINT source modules (Phases 10-16)
|
- Roadmap: Per-source rate limiter architecture (Phase 9) must precede all OSINT source modules (Phases 10-16)
|
||||||
- Roadmap: AES-256 encryption added in Phase 1, not post-hoc — avoids migration complexity
|
- Roadmap: AES-256 encryption added in Phase 1, not post-hoc — avoids migration complexity
|
||||||
- Roadmap: Verification (Phase 5) requires consent prompt + LEGAL.md — not optional polish
|
- Roadmap: Verification (Phase 5) requires consent prompt + LEGAL.md — not optional polish
|
||||||
- [Phase 01-foundation]: tools.go with //go:build tools tag used to pin Phase 1 dependencies before production code imports them
|
- [Phase 01-foundation]: Storage 01-03: Argon2id selected over PBKDF2 — memory-hard RFC 9106 params, resolves STATE.md blocker
|
||||||
- [Phase 01-foundation]: modernc.org/sqlite v1.48.1 selected (resolved from @latest) — CGO-free constraint satisfied, newer than RESEARCH.md v1.35.x reference
|
- [Phase 01-foundation]: Storage 01-03: AES-256-GCM nonce prepended to ciphertext in single BLOB column — no separate nonce column needed
|
||||||
|
|
||||||
### Pending Todos
|
### Pending Todos
|
||||||
|
|
||||||
@@ -81,6 +81,6 @@ None yet.
|
|||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-04-04T21:06:08.656Z
|
Last session: 2026-04-04T21:07:04.654Z
|
||||||
Stopped at: Completed 01-01-PLAN.md — Go module initialized, all Phase 1 deps pinned, test scaffolding created
|
Stopped at: Completed 01-foundation-03-PLAN.md
|
||||||
Resume file: None
|
Resume file: None
|
||||||
|
|||||||
139
.planning/phases/01-foundation/01-03-SUMMARY.md
Normal file
139
.planning/phases/01-foundation/01-03-SUMMARY.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 03
|
||||||
|
subsystem: database
|
||||||
|
tags: [sqlite, aes-256-gcm, argon2id, encryption, storage, modernc-sqlite]
|
||||||
|
|
||||||
|
# Dependency graph
|
||||||
|
requires:
|
||||||
|
- phase: 01-foundation-01
|
||||||
|
provides: go.mod with modernc.org/sqlite and golang.org/x/crypto dependencies
|
||||||
|
|
||||||
|
provides:
|
||||||
|
- AES-256-GCM column encryption (Encrypt/Decrypt) with random nonce prepended
|
||||||
|
- Argon2id key derivation (DeriveKey/NewSalt) using RFC 9106 parameters
|
||||||
|
- SQLite database Open() with WAL mode and embedded schema migration
|
||||||
|
- Finding CRUD: SaveFinding encrypts key_value at boundary, ListFindings decrypts transparently
|
||||||
|
- MaskKey helper: first8...last4 display format
|
||||||
|
- schema.sql with findings, scans, settings tables and performance indexes
|
||||||
|
|
||||||
|
affects: [01-04-scanner, 01-05-cli, 17-dashboard, 18-telegram]
|
||||||
|
|
||||||
|
# Tech tracking
|
||||||
|
tech-stack:
|
||||||
|
added:
|
||||||
|
- modernc.org/sqlite v1.48.1 (pure Go SQLite, CGO-free)
|
||||||
|
- golang.org/x/crypto (argon2.IDKey for key derivation)
|
||||||
|
- crypto/aes + crypto/cipher (stdlib AES-256-GCM)
|
||||||
|
patterns:
|
||||||
|
- "Encrypt-at-boundary: SaveFinding encrypts, ListFindings decrypts — storage layer handles all crypto transparently"
|
||||||
|
- "go:embed schema.sql — schema migrated on Open(), idempotent via CREATE TABLE IF NOT EXISTS"
|
||||||
|
- "WAL mode enabled on every Open() for concurrent read performance"
|
||||||
|
- "NULL scan_id: zero-value ScanID stored as SQL NULL to satisfy FK constraint"
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- pkg/storage/encrypt.go
|
||||||
|
- pkg/storage/crypto.go
|
||||||
|
- pkg/storage/db.go
|
||||||
|
- pkg/storage/findings.go
|
||||||
|
- pkg/storage/schema.sql
|
||||||
|
- pkg/storage/db_test.go
|
||||||
|
modified: []
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Argon2id over PBKDF2: RFC 9106 recommended, memory-hard, resolves blocker from STATE.md"
|
||||||
|
- "NULL scan_id for findings without parent scan — FK constraint satisfied without mandatory scan creation"
|
||||||
|
- "Nonce prepended to ciphertext in single []byte — simplifies storage (no separate column needed)"
|
||||||
|
- "MaskKey returns first8...last4 — consistent with plan spec, 12-char minimum before masking"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Pattern: Encrypt-at-boundary — pkg/storage is the only layer that sees encrypted bytes"
|
||||||
|
- "Pattern: sql.NullInt64 for nullable FK columns in scan results"
|
||||||
|
- "Pattern: go:embed for all embedded assets — schema.sql embedded in db.go"
|
||||||
|
|
||||||
|
requirements-completed: [STOR-01, STOR-02, STOR-03]
|
||||||
|
|
||||||
|
# Metrics
|
||||||
|
duration: 3min
|
||||||
|
completed: 2026-04-04
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 1 Plan 3: Storage Layer Summary
|
||||||
|
|
||||||
|
**AES-256-GCM column encryption with Argon2id key derivation and SQLite CRUD — raw BLOB verified to contain no plaintext key data**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Duration:** 3 min
|
||||||
|
- **Started:** 2026-04-04T21:02:00Z
|
||||||
|
- **Completed:** 2026-04-04T21:06:06Z
|
||||||
|
- **Tasks:** 2
|
||||||
|
- **Files modified:** 7 (5 created, go.mod + go.sum updated)
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- AES-256-GCM Encrypt/Decrypt with prepended random nonce — non-deterministic, wrong-key fails with GCM auth error
|
||||||
|
- Argon2id DeriveKey using RFC 9106 Section 7.3 params (time=1, memory=64MB, threads=4, keyLen=32) — resolves the Argon2 vs PBKDF2 blocker from STATE.md
|
||||||
|
- SQLite opens with WAL mode, foreign keys, and embedded schema.sql migration — works with `:memory:` for tests
|
||||||
|
- SaveFinding/ListFindings transparently encrypt/decrypt key_value at storage boundary
|
||||||
|
- All 7 tests pass including raw-BLOB assertion confirming plaintext is not stored
|
||||||
|
|
||||||
|
## Task Commits
|
||||||
|
|
||||||
|
Each task was committed atomically:
|
||||||
|
|
||||||
|
1. **TDD RED: Failing test suite** - `2ef54f7` (test)
|
||||||
|
2. **Task 1: AES-256-GCM + Argon2id** - `239e2c2` (feat)
|
||||||
|
3. **Task 2: SQLite DB + schema + CRUD** - `3334633` (feat)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
- `pkg/storage/encrypt.go` - Encrypt(plaintext, key) and Decrypt(ciphertext, key) using AES-256-GCM
|
||||||
|
- `pkg/storage/crypto.go` - DeriveKey(passphrase, salt) using Argon2id RFC 9106, NewSalt() 16-byte random
|
||||||
|
- `pkg/storage/db.go` - DB struct with Open(), Close(), SQL() — WAL mode, FK, embedded schema migration
|
||||||
|
- `pkg/storage/findings.go` - Finding struct, SaveFinding, ListFindings, MaskKey helper
|
||||||
|
- `pkg/storage/schema.sql` - CREATE TABLE for findings, scans, settings + 3 indexes
|
||||||
|
- `pkg/storage/db_test.go` - 7 tests including raw-BLOB encryption verification
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
- Argon2id selected over PBKDF2 (resolves STATE.md blocker) — memory-hard, RFC 9106 recommended
|
||||||
|
- NULL scan_id: zero-value ScanID stored as SQL NULL so findings can exist without a parent scan
|
||||||
|
- Single []byte for nonce+ciphertext — no separate nonce column needed, simplifies schema
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. [Rule 1 - Bug] FK constraint failed when ScanID = 0**
|
||||||
|
- **Found during:** Task 2 (TestSaveFindingEncrypted)
|
||||||
|
- **Issue:** Go zero value `ScanID: 0` sent as integer 0 to SQLite, failing FK constraint (no scan with id=0)
|
||||||
|
- **Fix:** SaveFinding converts zero ScanID to sql.NullInt64{} (NULL), ListFindings uses sql.NullInt64 for scan
|
||||||
|
- **Files modified:** pkg/storage/findings.go
|
||||||
|
- **Verification:** TestSaveFindingEncrypted passes after fix
|
||||||
|
- **Committed in:** 3334633 (Task 2 commit)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Total deviations:** 1 auto-fixed (Rule 1 - Bug)
|
||||||
|
**Impact on plan:** Necessary correctness fix — plan spec allows NULL scan_id (no NOT NULL in schema). No scope creep.
|
||||||
|
|
||||||
|
## Issues Encountered
|
||||||
|
|
||||||
|
None beyond the FK constraint bug documented above.
|
||||||
|
|
||||||
|
## User Setup Required
|
||||||
|
|
||||||
|
None - no external service configuration required.
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
- Storage layer fully functional with transparent encryption
|
||||||
|
- pkg/storage exports: Encrypt, Decrypt, DeriveKey, NewSalt, Open, DB, Finding, SaveFinding, ListFindings, MaskKey
|
||||||
|
- Scanner (Plan 04) can call SaveFinding to persist findings
|
||||||
|
- CLI (Plan 05) can call ListFindings to display findings
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 01-foundation*
|
||||||
|
*Completed: 2026-04-04*
|
||||||
39
go.mod
39
go.mod
@@ -2,52 +2,21 @@ module github.com/salvacybersec/keyhunter
|
|||||||
|
|
||||||
go 1.26.1
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require github.com/stretchr/testify v1.11.1
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
|
||||||
github.com/panjf2000/ants/v2 v2.12.0
|
|
||||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745
|
|
||||||
github.com/spf13/cobra v1.10.2
|
|
||||||
github.com/spf13/viper v1.21.0
|
|
||||||
github.com/stretchr/testify v1.11.1
|
|
||||||
golang.org/x/crypto v0.49.0
|
|
||||||
golang.org/x/time v0.15.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
|
||||||
modernc.org/sqlite v1.48.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
golang.org/x/crypto v0.49.0 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
|
||||||
golang.org/x/sync v0.20.0 // indirect
|
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.70.0 // indirect
|
modernc.org/libc v1.70.0 // indirect
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
|
modernc.org/sqlite v1.48.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
105
go.sum
105
go.sum
@@ -1,136 +1,33 @@
|
|||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
|
||||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||||
github.com/panjf2000/ants/v2 v2.12.0 h1:u9JhESo83i/GkZnhfTNuFMMWcNt7mnV1bGJ6FT4wXH8=
|
|
||||||
github.com/panjf2000/ants/v2 v2.12.0/go.mod h1:tSQuaNQ6r6NRhPt+IZVUevvDyFMTs+eS4ztZc52uJTY=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
|
||||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 h1:Vpr4VgAizEgEZsaMohpw6JYDP+i9Of9dmdY4ufNP6HI=
|
|
||||||
github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745/go.mod h1:EHPiTAKtiFmrMldLUNswFwfZ2eJIYBHktdaUTZxYWRw=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
|
||||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
|
||||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
|
||||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
|
||||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
|
||||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
|
||||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
|
||||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
|
||||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
|
||||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
|
||||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
|
||||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
|
||||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
|
|
||||||
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
|
|
||||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
|
||||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
|
||||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
|
||||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
|
||||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
|
||||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
|
||||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
|
||||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
|
||||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
|
||||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
|
||||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
|
||||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
|
||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
|
||||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|
||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
|
||||||
modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA=
|
modernc.org/sqlite v1.48.1 h1:S85iToyU6cgeojybE2XJlSbcsvcWkQ6qqNXJHtW5hWA=
|
||||||
modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
modernc.org/sqlite v1.48.1/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
|
||||||
|
|||||||
32
pkg/storage/crypto.go
Normal file
32
pkg/storage/crypto.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/argon2"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
argon2Time uint32 = 1
|
||||||
|
argon2Memory uint32 = 64 * 1024 // 64 MB — RFC 9106 Section 7.3
|
||||||
|
argon2Threads uint8 = 4
|
||||||
|
argon2KeyLen uint32 = 32 // AES-256 key length
|
||||||
|
saltSize = 16
|
||||||
|
)
|
||||||
|
|
||||||
|
// DeriveKey produces a 32-byte AES-256 key from a passphrase and salt using Argon2id.
|
||||||
|
// Uses RFC 9106 Section 7.3 recommended parameters.
|
||||||
|
// Given the same passphrase and salt, always returns the same key.
|
||||||
|
func DeriveKey(passphrase []byte, salt []byte) []byte {
|
||||||
|
return argon2.IDKey(passphrase, salt, argon2Time, argon2Memory, argon2Threads, argon2KeyLen)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSalt generates a cryptographically random 16-byte salt.
|
||||||
|
// Store alongside the database and reuse on each key derivation.
|
||||||
|
func NewSalt() ([]byte, error) {
|
||||||
|
salt := make([]byte, saltSize)
|
||||||
|
if _, err := rand.Read(salt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return salt, nil
|
||||||
|
}
|
||||||
57
pkg/storage/db.go
Normal file
57
pkg/storage/db.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &DB{sql: sqlDB}, 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
|
||||||
|
}
|
||||||
@@ -1,23 +1,127 @@
|
|||||||
package storage_test
|
package storage_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/salvacybersec/keyhunter/pkg/storage"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestDBOpen verifies SQLite database opens and creates schema.
|
|
||||||
// Stub: will be implemented when db.go exists (Plan 03).
|
|
||||||
func TestDBOpen(t *testing.T) {
|
func TestDBOpen(t *testing.T) {
|
||||||
t.Skip("stub — implement after db.go exists")
|
db, err := storage.Open(":memory:")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Verify schema tables exist
|
||||||
|
rows, err := db.SQL().Query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var tables []string
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
require.NoError(t, rows.Scan(&name))
|
||||||
|
tables = append(tables, name)
|
||||||
|
}
|
||||||
|
assert.Contains(t, tables, "findings")
|
||||||
|
assert.Contains(t, tables, "scans")
|
||||||
|
assert.Contains(t, tables, "settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestEncryptDecryptRoundtrip verifies AES-256-GCM encrypt/decrypt roundtrip.
|
|
||||||
// Stub: will be implemented when encrypt.go exists (Plan 03).
|
|
||||||
func TestEncryptDecryptRoundtrip(t *testing.T) {
|
func TestEncryptDecryptRoundtrip(t *testing.T) {
|
||||||
t.Skip("stub — implement after encrypt.go exists")
|
key := make([]byte, 32) // all-zero key for test
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i)
|
||||||
|
}
|
||||||
|
plaintext := []byte("sk-proj-supersecretapikey1234")
|
||||||
|
|
||||||
|
ciphertext, err := storage.Encrypt(plaintext, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Greater(t, len(ciphertext), len(plaintext), "ciphertext should be longer than plaintext")
|
||||||
|
|
||||||
|
recovered, err := storage.Decrypt(ciphertext, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, plaintext, recovered)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestArgon2KeyDerivation verifies Argon2id produces 32-byte key deterministically.
|
func TestEncryptNonDeterministic(t *testing.T) {
|
||||||
// Stub: will be implemented when crypto.go exists (Plan 03).
|
key := make([]byte, 32)
|
||||||
func TestArgon2KeyDerivation(t *testing.T) {
|
plain := []byte("test-key")
|
||||||
t.Skip("stub — implement after crypto.go exists")
|
ct1, err1 := storage.Encrypt(plain, key)
|
||||||
|
ct2, err2 := storage.Encrypt(plain, key)
|
||||||
|
require.NoError(t, err1)
|
||||||
|
require.NoError(t, err2)
|
||||||
|
assert.NotEqual(t, ct1, ct2, "same plaintext encrypted twice should produce different ciphertext")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecryptWrongKey(t *testing.T) {
|
||||||
|
key1 := make([]byte, 32)
|
||||||
|
key2 := make([]byte, 32)
|
||||||
|
key2[0] = 0xFF
|
||||||
|
|
||||||
|
ct, err := storage.Encrypt([]byte("secret"), key1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, err = storage.Decrypt(ct, key2)
|
||||||
|
assert.Error(t, err, "decryption with wrong key should fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgon2KeyDerivation(t *testing.T) {
|
||||||
|
passphrase := []byte("my-secure-passphrase")
|
||||||
|
salt := []byte("1234567890abcdef") // 16 bytes
|
||||||
|
|
||||||
|
key1 := storage.DeriveKey(passphrase, salt)
|
||||||
|
key2 := storage.DeriveKey(passphrase, salt)
|
||||||
|
|
||||||
|
assert.Equal(t, 32, len(key1), "derived key must be 32 bytes")
|
||||||
|
assert.Equal(t, key1, key2, "same passphrase+salt must produce same key")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewSalt(t *testing.T) {
|
||||||
|
salt1, err1 := storage.NewSalt()
|
||||||
|
salt2, err2 := storage.NewSalt()
|
||||||
|
require.NoError(t, err1)
|
||||||
|
require.NoError(t, err2)
|
||||||
|
assert.Equal(t, 16, len(salt1))
|
||||||
|
assert.NotEqual(t, salt1, salt2, "two salts should differ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSaveFindingEncrypted(t *testing.T) {
|
||||||
|
db, err := storage.Open(":memory:")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Derive a test key
|
||||||
|
key := storage.DeriveKey([]byte("testpassphrase"), []byte("testsalt1234xxxx"))
|
||||||
|
|
||||||
|
plainKey := "sk-proj-test1234567890abcdefghijklmnopqr"
|
||||||
|
f := storage.Finding{
|
||||||
|
ProviderName: "openai",
|
||||||
|
KeyValue: plainKey,
|
||||||
|
Confidence: "high",
|
||||||
|
SourcePath: "/test/file.env",
|
||||||
|
SourceType: "file",
|
||||||
|
LineNumber: 42,
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := db.SaveFinding(f, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Greater(t, id, int64(0))
|
||||||
|
|
||||||
|
findings, err := db.ListFindings(key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, findings, 1)
|
||||||
|
assert.Equal(t, plainKey, findings[0].KeyValue)
|
||||||
|
assert.Equal(t, "openai", findings[0].ProviderName)
|
||||||
|
// Verify masking
|
||||||
|
assert.Equal(t, "sk-proj-...opqr", findings[0].KeyMasked)
|
||||||
|
|
||||||
|
// Verify encryption contract: raw BLOB bytes in the database must NOT contain the plaintext key.
|
||||||
|
// This confirms Encrypt() was called before INSERT, not that the key was stored verbatim.
|
||||||
|
var rawBlob []byte
|
||||||
|
require.NoError(t, db.SQL().QueryRow("SELECT key_value FROM findings WHERE id = ?", id).Scan(&rawBlob))
|
||||||
|
assert.False(t, bytes.Contains(rawBlob, []byte(plainKey)),
|
||||||
|
"raw database BLOB must not contain plaintext key — encryption was not applied")
|
||||||
}
|
}
|
||||||
|
|||||||
52
pkg/storage/encrypt.go
Normal file
52
pkg/storage/encrypt.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrCiphertextTooShort is returned when ciphertext is shorter than the GCM nonce size.
|
||||||
|
var ErrCiphertextTooShort = errors.New("ciphertext too short")
|
||||||
|
|
||||||
|
// Encrypt encrypts plaintext using AES-256-GCM with a random nonce.
|
||||||
|
// The nonce is prepended to the returned ciphertext.
|
||||||
|
// key must be exactly 32 bytes (AES-256).
|
||||||
|
func Encrypt(plaintext []byte, key []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonce := make([]byte, gcm.NonceSize())
|
||||||
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Seal appends encrypted data to nonce, so nonce is prepended
|
||||||
|
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
||||||
|
return ciphertext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts ciphertext produced by Encrypt.
|
||||||
|
// Expects the nonce to be prepended to the ciphertext.
|
||||||
|
func Decrypt(ciphertext []byte, key []byte) ([]byte, error) {
|
||||||
|
block, err := aes.NewCipher(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
gcm, err := cipher.NewGCM(block)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonceSize := gcm.NonceSize()
|
||||||
|
if len(ciphertext) < nonceSize {
|
||||||
|
return nil, ErrCiphertextTooShort
|
||||||
|
}
|
||||||
|
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
||||||
|
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||||
|
}
|
||||||
103
pkg/storage/findings.go
Normal file
103
pkg/storage/findings.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Finding represents a detected API key with metadata.
|
||||||
|
// KeyValue is always plaintext in this struct — encryption happens at the storage boundary.
|
||||||
|
type Finding struct {
|
||||||
|
ID int64
|
||||||
|
ScanID int64
|
||||||
|
ProviderName string
|
||||||
|
KeyValue string // plaintext — encrypted before storage, decrypted after retrieval
|
||||||
|
KeyMasked string // first8...last4, stored plaintext
|
||||||
|
Confidence string
|
||||||
|
SourcePath string
|
||||||
|
SourceType string
|
||||||
|
LineNumber int
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaskKey returns the masked form of a key: first 8 chars + "..." + last 4 chars.
|
||||||
|
// If the key is too short (< 12 chars), returns the full key masked with asterisks.
|
||||||
|
func MaskKey(key string) string {
|
||||||
|
if len(key) < 12 {
|
||||||
|
return "****"
|
||||||
|
}
|
||||||
|
return key[:8] + "..." + key[len(key)-4:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SaveFinding encrypts the finding's KeyValue and persists the finding to the database.
|
||||||
|
// encKey must be a 32-byte AES-256 key (from DeriveKey).
|
||||||
|
func (db *DB) SaveFinding(f Finding, encKey []byte) (int64, error) {
|
||||||
|
encrypted, err := Encrypt([]byte(f.KeyValue), encKey)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("encrypting key value: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
masked := f.KeyMasked
|
||||||
|
if masked == "" {
|
||||||
|
masked = MaskKey(f.KeyValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use NULL for scan_id when not set (zero value) to satisfy FK constraint
|
||||||
|
var scanID interface{}
|
||||||
|
if f.ScanID != 0 {
|
||||||
|
scanID = f.ScanID
|
||||||
|
} else {
|
||||||
|
scanID = sql.NullInt64{}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := db.sql.Exec(
|
||||||
|
`INSERT INTO findings (scan_id, provider_name, key_value, key_masked, confidence, source_path, source_type, line_number)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
scanID, f.ProviderName, encrypted, masked, f.Confidence, f.SourcePath, f.SourceType, f.LineNumber,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("inserting finding: %w", err)
|
||||||
|
}
|
||||||
|
return res.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListFindings retrieves all findings, decrypting key values using encKey.
|
||||||
|
// encKey must be the same 32-byte key used during SaveFinding.
|
||||||
|
func (db *DB) ListFindings(encKey []byte) ([]Finding, error) {
|
||||||
|
rows, err := db.sql.Query(
|
||||||
|
`SELECT id, scan_id, provider_name, key_value, key_masked, confidence,
|
||||||
|
source_path, source_type, line_number, created_at
|
||||||
|
FROM findings ORDER BY created_at DESC`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("querying findings: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var findings []Finding
|
||||||
|
for rows.Next() {
|
||||||
|
var f Finding
|
||||||
|
var encrypted []byte
|
||||||
|
var createdAt string
|
||||||
|
var scanID sql.NullInt64
|
||||||
|
err := rows.Scan(
|
||||||
|
&f.ID, &scanID, &f.ProviderName, &encrypted, &f.KeyMasked,
|
||||||
|
&f.Confidence, &f.SourcePath, &f.SourceType, &f.LineNumber, &createdAt,
|
||||||
|
)
|
||||||
|
if scanID.Valid {
|
||||||
|
f.ScanID = scanID.Int64
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("scanning finding row: %w", err)
|
||||||
|
}
|
||||||
|
plain, err := Decrypt(encrypted, encKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("decrypting finding %d: %w", f.ID, err)
|
||||||
|
}
|
||||||
|
f.KeyValue = string(plain)
|
||||||
|
f.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||||
|
findings = append(findings, f)
|
||||||
|
}
|
||||||
|
return findings, rows.Err()
|
||||||
|
}
|
||||||
35
pkg/storage/schema.sql
Normal file
35
pkg/storage/schema.sql
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
-- KeyHunter database schema
|
||||||
|
-- Version: 1
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS scans (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
started_at DATETIME NOT NULL,
|
||||||
|
finished_at DATETIME,
|
||||||
|
source_path TEXT,
|
||||||
|
finding_count INTEGER DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS findings (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
scan_id INTEGER REFERENCES scans(id),
|
||||||
|
provider_name TEXT NOT NULL,
|
||||||
|
key_value BLOB NOT NULL,
|
||||||
|
key_masked TEXT NOT NULL,
|
||||||
|
confidence TEXT NOT NULL,
|
||||||
|
source_path TEXT,
|
||||||
|
source_type TEXT,
|
||||||
|
line_number INTEGER,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for common queries
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_findings_scan_id ON findings(scan_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_findings_provider ON findings(provider_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_findings_created ON findings(created_at DESC);
|
||||||
Reference in New Issue
Block a user