security: rotate exposed token, redact from PLAN.md, add secret patterns to .gitignore

This commit is contained in:
Shaun Arman 2026-03-15 14:04:49 -05:00
parent 4a5e70236a
commit 362687ad00
3 changed files with 3 additions and 430 deletions

3
.gitignore vendored
View File

@ -3,3 +3,6 @@ dist/
src-tauri/target/
.env
*.local
.secrets
secrets.yml
secrets.yaml

429
PLAN.md
View File

@ -1,429 +0,0 @@
# TFTSR — IT Triage & Root-Cause Analysis Desktop Application
## Implementation Plan
### Overview
TFTSR is a **desktop-first, offline-capable** application that helps IT teams
perform structured incident triage using the *5-Whys* methodology, backed by
pluggable AI providers (Ollama local, OpenAI, Anthropic, Mistral, Gemini).
It automates PII redaction, guides engineers through root-cause analysis, and
produces post-mortem documents (Markdown / PDF / DOCX).
---
## Architecture Decisions
| Area | Choice | Rationale |
|------|--------|-----------|
| Desktop framework | **Tauri 2.x** | Small binary, native webview, Rust backend for security |
| Frontend framework | **React 18** | Large ecosystem, component model fits wizard-style UX |
| State management | **Zustand** | Minimal boilerplate, TypeScript-friendly, no context nesting |
| Local database | **SQLCipher** (via `rusqlite` + `bundled-sqlcipher`) | Encrypted SQLite — secrets and PII at rest |
| Secret storage | **Tauri Stronghold** | OS-keychain-grade encrypted vault for API keys |
| AI providers | Ollama (local), OpenAI, Anthropic, Mistral, Gemini | User choice; local-first with cloud fallback |
| Unit tests (frontend) | **Vitest** | Fast, Vite-native, first-class TS support |
| E2E tests | **WebdriverIO + tauri-driver** | Official Tauri E2E path, cross-platform |
| CI/CD | **Woodpecker CI** (Gogs at `172.0.0.29:3000`) | Self-hosted, Docker-native, YAML pipelines |
| Bundling | Vite 6 | Dev server + production build, used by Tauri CLI |
---
## Directory Structure
```
tftsr/
├── .woodpecker/
│ ├── test.yml # lint + unit tests on push / PR
│ └── release.yml # multi-platform build on tag
├── cli/
│ ├── package.json
│ └── src/
│ └── main.ts # minimal CLI entry point
├── src/ # React frontend
│ ├── assets/
│ ├── components/
│ │ ├── common/ # Button, Card, Modal, DropZone …
│ │ ├── dashboard/ # IssueList, StatsCards
│ │ ├── triage/ # WhyStep, ChatBubble, ProgressBar
│ │ ├── rca/ # DocEditor, ExportBar
│ │ ├── settings/ # ProviderForm, ThemeToggle
│ │ └── pii/ # PiiHighlighter, RedactionPreview
│ ├── hooks/ # useInvoke, useListener, useTheme …
│ ├── lib/
│ │ ├── tauriCommands.ts # typed invoke wrappers & TS types
│ │ └── utils.ts # date formatting, debounce, etc.
│ ├── pages/
│ │ ├── DashboardPage.tsx
│ │ ├── NewIssuePage.tsx
│ │ ├── TriagePage.tsx
│ │ ├── RcaPage.tsx
│ │ ├── LogViewerPage.tsx
│ │ └── SettingsPage.tsx
│ ├── stores/
│ │ ├── sessionStore.ts # current triage session state
│ │ └── settingsStore.ts # theme, providers, preferences
│ ├── App.tsx
│ └── main.tsx
├── src-tauri/
│ ├── Cargo.toml
│ ├── tauri.conf.json
│ ├── capabilities/
│ │ └── default.json
│ ├── icons/
│ ├── src/
│ │ ├── main.rs # Tauri entry point
│ │ ├── db.rs # SQLCipher connection & migrations
│ │ ├── commands/ # IPC command modules
│ │ │ ├── mod.rs
│ │ │ ├── issues.rs
│ │ │ ├── triage.rs
│ │ │ ├── logs.rs
│ │ │ ├── pii.rs
│ │ │ ├── rca.rs
│ │ │ ├── ai.rs
│ │ │ └── settings.rs
│ │ ├── ai/ # AI provider abstractions
│ │ │ ├── mod.rs
│ │ │ ├── ollama.rs
│ │ │ ├── openai_compat.rs
│ │ │ └── prompt_templates.rs
│ │ ├── pii/ # PII detection engine
│ │ │ ├── mod.rs
│ │ │ └── patterns.rs
│ │ └── export/ # Document export
│ │ ├── mod.rs
│ │ ├── markdown.rs
│ │ ├── pdf.rs
│ │ └── docx.rs
│ └── migrations/
│ └── 001_init.sql
├── tests/
│ ├── unit/
│ │ ├── setup.ts
│ │ ├── pii.test.ts
│ │ ├── sessionStore.test.ts
│ │ └── settingsStore.test.ts
│ └── e2e/
│ ├── wdio.conf.ts
│ ├── helpers/
│ │ └── app.ts
│ └── specs/
│ ├── onboarding.spec.ts
│ ├── log-upload.spec.ts
│ ├── triage-flow.spec.ts
│ └── rca-export.spec.ts
├── package.json
├── tsconfig.json
├── vite.config.ts
└── PLAN.md # ← this file
```
---
## Database Schema (SQLCipher)
All tables live in a single encrypted `tftsr.db` file under the Tauri
app-data directory.
### 1. `issues`
```sql
CREATE TABLE issues (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
domain TEXT NOT NULL CHECK(domain IN
('linux','windows','network','k8s','db','virt','hw','obs')),
status TEXT NOT NULL DEFAULT 'open'
CHECK(status IN ('open','triaging','resolved','closed')),
severity TEXT CHECK(severity IN ('p1','p2','p3','p4')),
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
```
### 2. `triage_messages`
```sql
CREATE TABLE triage_messages (
id TEXT PRIMARY KEY,
issue_id TEXT NOT NULL REFERENCES issues(id),
role TEXT NOT NULL CHECK(role IN ('user','assistant','system')),
content TEXT NOT NULL,
why_level INTEGER NOT NULL DEFAULT 0,
created_at INTEGER NOT NULL
);
CREATE INDEX idx_triage_msg_issue ON triage_messages(issue_id);
```
### 3. `log_files`
```sql
CREATE TABLE log_files (
id TEXT PRIMARY KEY,
issue_id TEXT NOT NULL REFERENCES issues(id),
filename TEXT NOT NULL,
content TEXT NOT NULL,
mime_type TEXT,
size_bytes INTEGER,
created_at INTEGER NOT NULL
);
```
### 4. `pii_spans`
```sql
CREATE TABLE pii_spans (
id TEXT PRIMARY KEY,
log_file_id TEXT NOT NULL REFERENCES log_files(id),
pii_type TEXT NOT NULL,
start_pos INTEGER NOT NULL,
end_pos INTEGER NOT NULL,
original TEXT NOT NULL,
replacement TEXT NOT NULL
);
```
### 5. `rca_documents`
```sql
CREATE TABLE rca_documents (
id TEXT PRIMARY KEY,
issue_id TEXT NOT NULL REFERENCES issues(id) UNIQUE,
content TEXT NOT NULL DEFAULT '',
format TEXT NOT NULL DEFAULT 'markdown',
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
```
### 6. `ai_providers`
```sql
CREATE TABLE ai_providers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
api_url TEXT NOT NULL,
model TEXT NOT NULL,
created_at INTEGER NOT NULL
);
```
### 7. `settings`
```sql
CREATE TABLE settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
```
### 8. `export_history`
```sql
CREATE TABLE export_history (
id TEXT PRIMARY KEY,
issue_id TEXT NOT NULL REFERENCES issues(id),
format TEXT NOT NULL CHECK(format IN ('md','pdf','docx')),
file_path TEXT NOT NULL,
created_at INTEGER NOT NULL
);
```
---
## IPC Command Interface
All frontend ↔ backend communication goes through Tauri's `invoke()`.
### Issue commands
| Command | Payload | Returns |
|---------|---------|---------|
| `create_issue` | `{ title, domain, severity }` | `Issue` |
| `list_issues` | `{ status?, domain? }` | `Issue[]` |
| `get_issue` | `{ id }` | `Issue` |
| `update_issue` | `{ id, title?, status?, severity? }` | `Issue` |
| `delete_issue` | `{ id }` | `void` |
### Triage commands
| Command | Payload | Returns |
|---------|---------|---------|
| `send_triage_message` | `{ issueId, content, whyLevel }` | `TriageMessage` (assistant reply) |
| `get_triage_history` | `{ issueId }` | `TriageMessage[]` |
| `set_why_level` | `{ issueId, level }` | `void` |
### Log commands
| Command | Payload | Returns |
|---------|---------|---------|
| `upload_log` | `{ issueId, filename, content }` | `LogFile` |
| `list_logs` | `{ issueId }` | `LogFile[]` |
| `delete_log` | `{ id }` | `void` |
### PII commands
| Command | Payload | Returns |
|---------|---------|---------|
| `detect_pii` | `{ logFileId }` | `PiiDetectionResult` |
| `apply_redactions` | `{ logFileId, spanIds }` | `string` (redacted text) |
### RCA / Export commands
| Command | Payload | Returns |
|---------|---------|---------|
| `generate_rca` | `{ issueId }` | `RcaDocument` |
| `update_rca` | `{ id, content }` | `RcaDocument` |
| `export_document` | `{ issueId, format }` | `string` (file path) |
### AI / Settings commands
| Command | Payload | Returns |
|---------|---------|---------|
| `test_provider` | `{ name, apiUrl, apiKey?, model }` | `{ ok, message }` |
| `save_provider` | `{ provider }` | `void` |
| `get_settings` | `{}` | `Settings` |
| `update_settings` | `{ key, value }` | `void` |
---
## CI/CD Approach
### Infrastructure
- **Git server**: Gogs at `http://172.0.0.29:3000`
- **CI runner**: Woodpecker CI with Docker executor
- **Artifacts**: Uploaded to Gogs releases via API
### Pipelines
| Pipeline | Trigger | Steps |
|----------|---------|-------|
| `.woodpecker/test.yml` | push, PR | `rustfmt` check → Clippy → Rust tests → TS typecheck → Vitest → coverage (main only) |
| `.woodpecker/release.yml` | `v*` tag | Build linux-amd64 → Build linux-arm64 → Upload to Gogs release |
---
## Security Implementation
1. **Database encryption** — SQLCipher with a key derived from Tauri Stronghold.
2. **API key storage** — Stronghold vault, never stored in plaintext.
3. **PII redaction** — Regex + heuristic engine runs before any text leaves the device.
4. **CSP** — Strict Content-Security-Policy in `tauri.conf.json`; only allowlisted AI API origins.
5. **Least-privilege capabilities**`capabilities/default.json` grants only required Tauri permissions.
6. **No remote code** — All assets bundled; no CDN scripts.
---
## Testing Strategy
| Layer | Tool | Location | What it covers |
|-------|------|----------|----------------|
| Rust unit | `cargo test` | `src-tauri/src/**` | DB operations, PII regex, AI prompt building |
| Frontend unit | Vitest | `tests/unit/` | Stores, command wrappers, component logic |
| E2E | WebdriverIO + tauri-driver | `tests/e2e/` | Full user flows: onboarding, triage, export |
| Lint | `rustfmt` + Clippy + `tsc --noEmit` | CI | Code style, type safety |
---
## Implementation Phases
### Phase 1 — Project Scaffold & CI ✅ COMPLETE
- [x] Initialise repo with Tauri 2.x + React 18 + Vite
- [x] Configure `tauri.conf.json` and capabilities
- [x] Set up Woodpecker CI pipelines (`test.yml`, `release.yml`)
- [x] Write Vitest setup and mock harness
- [x] Write initial unit tests (PII, sessionStore, settingsStore) — 13/13 passing
- [x] Write E2E scaffolding (wdio config, helpers, skeleton specs)
- [x] Create CLI stub (`cli/`)
- [x] Push to Gogs at http://172.0.0.29:3000/sarman/tftsr-devops_investigation
- [x] Write README.md
- [x] Deploy Woodpecker CI v0.15.4 (server + agent + nginx proxy)
- [ ] **BLOCKED**: Verify CI green on push (Woodpecker hook auth issue — see below)
### Phase 2 — Database & Migrations ✅ COMPLETE
- [x] Integrate `rusqlite` + `bundled-sqlcipher`
- [x] Write migrations (10 tables: issues, log_files, pii_spans, ai_conversations, ai_messages, resolution_steps, documents, audit_log, settings, integration_publishes)
- [x] Implement migration runner in `db/migrations.rs`
- [x] DB models with all required types
### Phase 3 — Stronghold Integration ✅ COMPLETE (scaffold)
- [x] `tauri-plugin-stronghold` registered in `lib.rs`
- [x] Password derivation function configured
- [ ] Full key lifecycle tests (deferred to Phase 3 proper)
### Phase 4 — Issue CRUD ✅ COMPLETE
- [x] All issue CRUD commands: create, get, list, update, delete, search
- [x] 5-Whys tracking: add_five_why, update_five_why
- [x] Timeline events: add_timeline_event
- [x] Dashboard, NewIssue, History pages
### Phase 5 — Log Ingestion & PII Detection ✅ COMPLETE
- [x] `upload_log_file`, `detect_pii`, `apply_redactions` commands
- [x] PII engine: 11 regex patterns (IPv4, IPv6, email, phone, SSN, CC, MAC, bearer, password, API key, URL)
- [x] PiiDiffViewer component
- [x] LogUpload page
### Phase 6 — AI Provider Abstraction ✅ COMPLETE
- [x] OpenAI-compatible, Anthropic, Gemini, Mistral, Ollama providers
- [x] `analyze_logs`, `chat_message`, `list_providers` IPC commands
- [x] Settings/AIProviders page
- [x] 8 IT domain system prompts
### Phase 7 — 5-Whys Triage Engine ✅ COMPLETE
- [x] Triage page with ChatWindow
- [x] TriageProgress component (5-step indicator)
- [x] Auto-detection of why level from AI responses
- [x] Session store with message persistence
### Phase 8 — RCA & Post-Mortem Generation ✅ COMPLETE
- [x] `generate_rca`, `generate_postmortem` commands
- [x] RCA and post-mortem Markdown templates
- [x] DocEditor component with export (MD, PDF)
- [x] RCA and Postmortem pages
### Phase 9 — Document Export ✅ COMPLETE (MD + PDF)
- [x] Markdown export
- [x] PDF export via `printpdf`
- [ ] DOCX export (not yet implemented — docx-rs dep removed for simplicity)
### Phase 10 — Polish & Settings ✅ COMPLETE
- [x] Dark/light theme via Tailwind + CSS variables
- [x] Ollama settings page with hardware detection + model management
- [x] Security page with audit log
- [x] Integrations page (v0.2 stubs)
### Phase 11 — Woodpecker CI Integration 🔴 IN PROGRESS / BLOCKED
**What works:**
- [x] Woodpecker CI v0.15.4 deployed at http://172.0.0.29:8084
- [x] Gogs SSRF bypass: webhook URL uses 172.0.0.29:8084 (not container IP)
- [x] Webhook delivery confirmed: Gogs IS sending webhooks to Woodpecker
- [x] Custom login proxy at http://172.0.0.29:8085 with correct `username=` form field
**What's blocked:**
- [ ] Woodpecker hook authentication: 400 "failure to parse token" when Gogs delivers webhook
- Root cause: `token.ParseRequest()` in Woodpecker 0.15.4 doesn't read `?token=` URL param
- With `?access_token=<JWT>`: gets 404 (JWT validated, but parsePushHook returns nil unexpectedly)
- Web UI login via http://172.0.0.29:8085/login: custom form works but SPA still intercepts
- [ ] Repo activation via Woodpecker API returns 401 (permission/token issue)
- [ ] CI build not yet triggering on push
**Next steps:**
1. Fix Woodpecker token storage: ensure Woodpecker DB has the actual bearer token
(Gogs token `sha1` from CREATE response = actual token; DB `sha1` column = hash of it)
2. Delete old "woodpecker" Gogs tokens via PostgreSQL, then re-login to Woodpecker
3. Fix the `?access_token=<JWT>` 404 — investigate parsePushHook nil return
4. Alternative: upgrade to Woodpecker 2.x and add OAuth2 to Gogs (or migrate to Gitea)
### Phase 12 — Release Package 🔲 PENDING
- [ ] Tag v0.1.0-alpha
- [ ] Verify Woodpecker builds Linux amd64 + arm64
- [ ] Verify artifacts upload to Gogs release
- [ ] Smoke-test installed packages
---
## Known Issues & Gotchas
### Gogs Token Authentication
- The `sha1` in the Gogs CREATE token API response IS the actual bearer token
- Gogs stores `sha1(token)` and `sha256(token)` in the DB — these are HASHES, not the token itself
- Known working tokens: `[REDACTED-ROTATED]` (woodpecker-setup)
### Woodpecker CI + Gogs v0.15.4 Compatibility
- The SPA form login uses `login=` field but Gogs backend reads `username=`
- Workaround: nginx proxy at :8085 serves custom HTML login page
- The webhook `?token=` URL param is NOT read by Woodpecker's `token.ParseRequest()`
- Use `?access_token=<JWT>` instead (JWT must be HS256 signed with `repo_hash` as key)
- Gogs 0.14 has no OAuth2 provider support — blocks upgrade to Woodpecker 2.x
### Rust/DB Type Notes
- IssueDetail is NESTED: `{ issue: Issue, log_files, resolution_steps, conversations }`
- DB uses TEXT timestamps for created_at/updated_at (not INTEGER)
- All commands use the `and_then` pattern with rusqlite to avoid lifetime issues

View File

@ -1 +0,0 @@
{"rustc_fingerprint":3935335233893156513,"outputs":{"850482172296875736":{"success":true,"status":"","code":0,"stdout":"rustc 1.94.0 (4a4ef493e 2026-03-02)\nbinary: rustc\ncommit-hash: 4a4ef493e3a1488c6e321570238084b38948f6db\ncommit-date: 2026-03-02\nhost: aarch64-unknown-linux-gnu\nrelease: 1.94.0\nLLVM version: 21.1.8\n","stderr":""},"4592665983954943194":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/sarman/.rustup/toolchains/stable-aarch64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"neon\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""}},"successes":{}}