""" Layer 8 — Feedback Store (SQLite) Self-improving per-repo. Human corrections persist and skip re-evaluation on future scans. """ from __future__ import annotations import sqlite3 import hashlib import os import contextlib import threading from .models import Finding DEFAULT_DB_PATH = os.environ.get("FEEDBACK_DB_PATH", "/data/feedback.sqlite") FEEDBACK_DB = DEFAULT_DB_PATH _lock = threading.Lock() def get_db_path(db_path: str | None = None) -> str: if db_path is None: db_path = FEEDBACK_DB try: dir_name = os.path.dirname(db_path) if dir_name: os.makedirs(dir_name, exist_ok=True) # Try to open/create a test connection to verify access conn = sqlite3.connect(db_path) conn.close() return db_path except (PermissionError, sqlite3.OperationalError, OSError): fallback_path = "./.vibesec_feedback.sqlite" dir_name = os.path.dirname(fallback_path) if dir_name: os.makedirs(dir_name, exist_ok=True) return fallback_path def _ensure_schema(conn: sqlite3.Connection): conn.execute(""" CREATE TABLE IF NOT EXISTS dismissed_findings ( id INTEGER PRIMARY KEY AUTOINCREMENT, rule_id TEXT NOT NULL, file_role TEXT, code_hash TEXT NOT NULL, verdict TEXT NOT NULL CHECK(verdict IN ('false_positive', 'real')), confirmed_by TEXT NOT NULL CHECK(confirmed_by IN ('human', 'tier1', 'tier2', 'tier3')), timestamp DATETIME DEFAULT CURRENT_TIMESTAMP ) """) try: # Deduplicate existing rows before applying unique index to prevent IntegrityError conn.execute(""" DELETE FROM dismissed_findings WHERE id NOT IN ( SELECT MAX(id) FROM dismissed_findings GROUP BY rule_id, code_hash ) """) except Exception: pass try: conn.execute("DROP INDEX IF EXISTS idx_rule_hash") except Exception: pass conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_rule_hash ON dismissed_findings (rule_id, code_hash)") conn.execute(""" CREATE TABLE IF NOT EXISTS rule_fp_rates ( check_id TEXT, file_role TEXT, seen_count INTEGER DEFAULT 0, fp_count INTEGER DEFAULT 0, PRIMARY KEY (check_id, file_role) ) """) conn.commit() @contextlib.contextmanager def get_db_connection(): db_path = get_db_path() conn = sqlite3.connect(db_path) _ensure_schema(conn) conn.row_factory = lambda cursor, row: {col[0]: row[idx] for idx, col in enumerate(cursor.description)} try: yield conn finally: conn.close() def record_verdict(check_id: str, file_role: str, is_false_positive: bool): with _lock: with get_db_connection() as conn: cursor = conn.execute( "SELECT seen_count, fp_count FROM rule_fp_rates WHERE check_id = ? AND file_role = ?", (check_id, file_role), ) row = cursor.fetchone() if row: seen = row["seen_count"] + 1 fp = row["fp_count"] + (1 if is_false_positive else 0) conn.execute( "UPDATE rule_fp_rates SET seen_count = ?, fp_count = ? WHERE check_id = ? AND file_role = ?", (seen, fp, check_id, file_role), ) else: seen = 1 fp = 1 if is_false_positive else 0 conn.execute( "INSERT INTO rule_fp_rates (check_id, file_role, seen_count, fp_count) VALUES (?, ?, ?, ?)", (check_id, file_role, seen, fp), ) conn.commit() def get_high_fp_exclusions(threshold: float = 0.90, min_samples: int = 10): with get_db_connection() as conn: cursor = conn.execute( "SELECT check_id, file_role FROM rule_fp_rates WHERE seen_count >= ? AND (fp_count * 1.0 / seen_count) >= ?", (min_samples, threshold), ) return {(row["check_id"], row["file_role"]) for row in cursor.fetchall()} class FeedbackStore: def __init__(self, db_path: str = DEFAULT_DB_PATH): resolved_path = get_db_path(db_path) self.conn = sqlite3.connect(resolved_path, check_same_thread=False) self._init_schema() def _init_schema(self): _ensure_schema(self.conn) def _code_hash(self, finding: Finding) -> str: # TODO (Priority: HIGH) - ISSUE #108: Resolve Feedback Store Line Shift Vulnerability # Currently, the hash includes the exact line_number. If a file is modified (e.g. inserting lines at the top), # the line number of a previously-dismissed finding shifts. This results in a new hash, causing the # dismissed finding to reappear in future scans. # # Future Mitigation Plan: # 1. Update the dismissed_findings database schema to store the absolute file_path and line_number columns separately. # 2. Store the original code snippet (normalized content of the line) as a column. # 3. Implement fuzzy matching during verification: if a finding matches rule_id and file_path, check if a dismissed # finding exists within a +/- 5 lines tolerance range, or verify that the normalized line content is identical. content = f"{finding.check_id}:{finding.file_path}:{finding.line_number}" return hashlib.sha256(content.encode()).hexdigest()[:16] def is_known_false_positive(self, finding: Finding) -> bool: code_hash = self._code_hash(finding) rule_id = finding.check_id or "" row = self.conn.execute( "SELECT verdict FROM dismissed_findings WHERE rule_id=? AND code_hash=? ORDER BY timestamp DESC LIMIT 1", (rule_id, code_hash) ).fetchone() return row is not None and row[0] == "false_positive" def record_verdict( self, finding: Finding, verdict: str, confirmed_by: str = "human", ): code_hash = self._code_hash(finding) self.conn.execute(""" INSERT OR REPLACE INTO dismissed_findings (rule_id, file_role, code_hash, verdict, confirmed_by) VALUES (?, ?, ?, ?, ?) """, (finding.check_id or "", finding.file_role or "", code_hash, verdict, confirmed_by)) self.conn.commit() def close(self): self.conn.close()