2026-05-05 15:47:09 -06:00
|
|
|
-- =============================================================================
|
|
|
|
|
-- CVE Dashboard — Complete PostgreSQL Schema (v1.0.0)
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Translates the full SQLite schema (setup.js) to PostgreSQL 16.
|
|
|
|
|
-- Designed for idempotent execution: safe to run multiple times via psql or
|
|
|
|
|
-- pool.query() without errors or duplicate data.
|
|
|
|
|
--
|
|
|
|
|
-- Usage:
|
|
|
|
|
-- psql -h localhost -p 5433 -U steam -d cve_dashboard -f backend/db-schema.sql
|
|
|
|
|
-- OR
|
|
|
|
|
-- const schema = fs.readFileSync('backend/db-schema.sql', 'utf8');
|
|
|
|
|
-- await pool.query(schema);
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Core CVE tracking tables
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS cves (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
cve_id VARCHAR(20) NOT NULL,
|
|
|
|
|
vendor VARCHAR(100) NOT NULL,
|
|
|
|
|
severity VARCHAR(20) NOT NULL,
|
|
|
|
|
description TEXT,
|
|
|
|
|
published_date DATE,
|
|
|
|
|
status VARCHAR(50) DEFAULT 'Open',
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
created_by INTEGER,
|
|
|
|
|
UNIQUE(cve_id, vendor)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_cve_id ON cves(cve_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_vendor ON cves(vendor);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_severity ON cves(severity);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_status ON cves(status);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS documents (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
cve_id VARCHAR(20) NOT NULL,
|
|
|
|
|
vendor VARCHAR(100) NOT NULL,
|
|
|
|
|
name VARCHAR(255) NOT NULL,
|
|
|
|
|
type VARCHAR(50) NOT NULL,
|
|
|
|
|
file_path VARCHAR(500) NOT NULL,
|
|
|
|
|
file_size VARCHAR(20),
|
|
|
|
|
mime_type VARCHAR(100),
|
|
|
|
|
uploaded_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
notes TEXT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_doc_cve_id ON documents(cve_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_doc_vendor ON documents(vendor);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_doc_type ON documents(type);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS required_documents (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
vendor VARCHAR(100) NOT NULL,
|
|
|
|
|
document_type VARCHAR(50) NOT NULL,
|
|
|
|
|
is_mandatory BOOLEAN DEFAULT TRUE,
|
|
|
|
|
description TEXT,
|
|
|
|
|
UNIQUE(vendor, document_type)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Authentication and session management
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
username VARCHAR(50) UNIQUE NOT NULL,
|
|
|
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
|
|
|
password_hash VARCHAR(255) NOT NULL,
|
|
|
|
|
role VARCHAR(20) NOT NULL DEFAULT 'viewer' CHECK (role IN ('admin', 'editor', 'viewer')),
|
|
|
|
|
is_active BOOLEAN DEFAULT TRUE,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
last_login TIMESTAMPTZ,
|
|
|
|
|
user_group VARCHAR(20) NOT NULL DEFAULT 'Read_Only'
|
|
|
|
|
CHECK (user_group IN ('Admin', 'Standard_User', 'Leadership', 'Read_Only')),
|
|
|
|
|
bu_teams TEXT NOT NULL DEFAULT ''
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_users_username ON users(username);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_users_user_group ON users(user_group);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
session_id VARCHAR(255) UNIQUE NOT NULL,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_session_id ON sessions(session_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Audit logging
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
user_id INTEGER,
|
|
|
|
|
username VARCHAR(50) NOT NULL,
|
|
|
|
|
action VARCHAR(50) NOT NULL,
|
|
|
|
|
entity_type VARCHAR(50) NOT NULL,
|
|
|
|
|
entity_id VARCHAR(100),
|
|
|
|
|
details TEXT,
|
|
|
|
|
ip_address VARCHAR(45),
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_user_id ON audit_logs(user_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_action ON audit_logs(action);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_entity_type ON audit_logs(entity_type);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_logs(created_at);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Jira integration
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS jira_tickets (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
cve_id TEXT NOT NULL,
|
|
|
|
|
vendor TEXT NOT NULL,
|
|
|
|
|
ticket_key TEXT NOT NULL,
|
|
|
|
|
url TEXT,
|
|
|
|
|
summary TEXT,
|
|
|
|
|
status TEXT DEFAULT 'Open' CHECK (status IN ('Open', 'In Progress', 'Closed')),
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_jira_tickets_cve ON jira_tickets(cve_id, vendor);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_jira_tickets_status ON jira_tickets(status);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Archer integration
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS archer_tickets (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
exc_number TEXT NOT NULL UNIQUE,
|
|
|
|
|
archer_url TEXT,
|
|
|
|
|
status TEXT DEFAULT 'Draft' CHECK (status IN ('Draft', 'Open', 'Under Review', 'Accepted')),
|
|
|
|
|
cve_id TEXT NOT NULL,
|
|
|
|
|
vendor TEXT NOT NULL,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_archer_tickets_cve ON archer_tickets(cve_id, vendor);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_archer_tickets_status ON archer_tickets(status);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_archer_tickets_exc ON archer_tickets(exc_number);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Knowledge base
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS knowledge_base (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
title VARCHAR(255) NOT NULL,
|
|
|
|
|
slug VARCHAR(255) UNIQUE NOT NULL,
|
|
|
|
|
description TEXT,
|
|
|
|
|
category VARCHAR(100),
|
|
|
|
|
file_path VARCHAR(500),
|
|
|
|
|
file_name VARCHAR(255),
|
|
|
|
|
file_type VARCHAR(50),
|
|
|
|
|
file_size INTEGER,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
created_by INTEGER REFERENCES users(id)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_knowledge_base_slug ON knowledge_base(slug);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_knowledge_base_category ON knowledge_base(category);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_knowledge_base_created_at ON knowledge_base(created_at DESC);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Ivanti findings — individual rows (replaces findings_json blob)
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_findings (
|
|
|
|
|
id TEXT PRIMARY KEY,
|
|
|
|
|
host_id INTEGER,
|
|
|
|
|
title TEXT NOT NULL DEFAULT '',
|
|
|
|
|
severity NUMERIC(4,2) NOT NULL DEFAULT 0,
|
|
|
|
|
vrr_group TEXT NOT NULL DEFAULT '',
|
|
|
|
|
host_name TEXT NOT NULL DEFAULT '',
|
|
|
|
|
ip_address TEXT NOT NULL DEFAULT '',
|
|
|
|
|
dns TEXT NOT NULL DEFAULT '',
|
|
|
|
|
status TEXT NOT NULL DEFAULT '',
|
|
|
|
|
sla_status TEXT NOT NULL DEFAULT '',
|
|
|
|
|
due_date DATE,
|
|
|
|
|
last_found_on DATE,
|
|
|
|
|
bu_ownership TEXT NOT NULL DEFAULT '',
|
|
|
|
|
cves TEXT[] DEFAULT '{}',
|
|
|
|
|
workflow_id TEXT,
|
|
|
|
|
workflow_state TEXT,
|
|
|
|
|
workflow_type TEXT,
|
|
|
|
|
state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'closed')),
|
|
|
|
|
note TEXT NOT NULL DEFAULT '',
|
|
|
|
|
override_host_name TEXT,
|
|
|
|
|
override_dns TEXT,
|
|
|
|
|
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_findings_state ON ivanti_findings(state);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_findings_bu ON ivanti_findings(bu_ownership);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_findings_severity ON ivanti_findings(severity);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_findings_state_bu ON ivanti_findings(state, bu_ownership);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Ivanti sync state (single-row pattern — replaces ivanti_findings_cache metadata)
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_sync_state (
|
|
|
|
|
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
|
|
|
total INTEGER DEFAULT 0,
|
|
|
|
|
workflows_json TEXT DEFAULT '[]',
|
|
|
|
|
synced_at TIMESTAMPTZ,
|
|
|
|
|
sync_status TEXT DEFAULT 'never',
|
|
|
|
|
error_message TEXT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Ivanti counts cache (single-row pattern for FP workflow counts)
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_counts_cache (
|
|
|
|
|
id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1),
|
|
|
|
|
open_count INTEGER DEFAULT 0,
|
|
|
|
|
closed_count INTEGER DEFAULT 0,
|
|
|
|
|
synced_at TIMESTAMPTZ,
|
|
|
|
|
fp_workflow_counts_json TEXT DEFAULT '{}',
|
|
|
|
|
fp_id_counts_json TEXT DEFAULT '{}'
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Ivanti counts history
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_counts_history (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
open_count INTEGER NOT NULL,
|
|
|
|
|
closed_count INTEGER NOT NULL,
|
|
|
|
|
recorded_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-06 13:38:38 -06:00
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_counts_history_by_bu (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
bu_ownership TEXT NOT NULL,
|
|
|
|
|
state TEXT NOT NULL CHECK (state IN ('open', 'closed')),
|
|
|
|
|
count INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
recorded_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_counts_history_bu ON ivanti_counts_history_by_bu(bu_ownership);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_counts_history_bu_date ON ivanti_counts_history_by_bu(recorded_at);
|
|
|
|
|
|
2026-05-05 15:47:09 -06:00
|
|
|
-- =============================================================================
|
|
|
|
|
-- Ivanti FP (False Positive) submissions
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_fp_submissions (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
user_id INTEGER NOT NULL,
|
|
|
|
|
username TEXT NOT NULL,
|
|
|
|
|
ivanti_workflow_batch_id INTEGER,
|
|
|
|
|
ivanti_generated_id TEXT,
|
|
|
|
|
ivanti_workflow_batch_uuid TEXT,
|
|
|
|
|
workflow_name TEXT NOT NULL,
|
|
|
|
|
reason TEXT NOT NULL,
|
|
|
|
|
description TEXT,
|
|
|
|
|
expiration_date TEXT NOT NULL,
|
|
|
|
|
scope_override TEXT NOT NULL DEFAULT 'Authorized',
|
|
|
|
|
finding_ids_json TEXT NOT NULL,
|
|
|
|
|
queue_item_ids_json TEXT NOT NULL,
|
|
|
|
|
attachment_count INTEGER DEFAULT 0,
|
|
|
|
|
attachment_results_json TEXT,
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'success' CHECK (status IN ('success', 'partial', 'failed')),
|
|
|
|
|
lifecycle_status TEXT NOT NULL DEFAULT 'submitted'
|
|
|
|
|
CHECK (lifecycle_status IN ('submitted', 'approved', 'rejected', 'rework', 'resubmitted')),
|
|
|
|
|
error_message TEXT,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
updated_at TIMESTAMPTZ DEFAULT NULL
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_fp_submissions_user ON ivanti_fp_submissions(user_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_fp_submissions_ivanti_id ON ivanti_fp_submissions(ivanti_generated_id);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_fp_submission_history (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
submission_id INTEGER NOT NULL REFERENCES ivanti_fp_submissions(id) ON DELETE CASCADE,
|
|
|
|
|
user_id INTEGER NOT NULL,
|
|
|
|
|
username TEXT NOT NULL,
|
|
|
|
|
change_type TEXT NOT NULL CHECK (change_type IN (
|
|
|
|
|
'created', 'fields_updated', 'findings_added',
|
|
|
|
|
'attachments_added', 'status_changed'
|
|
|
|
|
)),
|
|
|
|
|
change_details_json TEXT,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_fp_history_submission ON ivanti_fp_submission_history(submission_id);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Ivanti todo queue (FP, Archer, CARD, GRANITE workflows)
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_todo_queue (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
|
|
|
finding_id TEXT NOT NULL,
|
|
|
|
|
finding_title TEXT,
|
|
|
|
|
cves_json TEXT,
|
|
|
|
|
ip_address TEXT,
|
|
|
|
|
hostname TEXT,
|
|
|
|
|
vendor TEXT NOT NULL,
|
|
|
|
|
workflow_type TEXT NOT NULL CHECK (workflow_type IN ('FP', 'Archer', 'CARD', 'GRANITE')),
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'complete')),
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_todo_queue_user ON ivanti_todo_queue(user_id, status);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Ivanti archive detection and anomaly tracking
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_finding_archives (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
finding_id TEXT NOT NULL UNIQUE,
|
|
|
|
|
finding_title TEXT NOT NULL DEFAULT '',
|
|
|
|
|
host_name TEXT NOT NULL DEFAULT '',
|
|
|
|
|
ip_address TEXT NOT NULL DEFAULT '',
|
|
|
|
|
current_state TEXT NOT NULL CHECK (current_state IN ('ARCHIVED', 'RETURNED', 'CLOSED', 'CLOSED_GONE')),
|
|
|
|
|
last_severity NUMERIC(4,2) NOT NULL DEFAULT 0,
|
|
|
|
|
first_archived_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
last_transition_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_archive_finding_id ON ivanti_finding_archives(finding_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_archive_current_state ON ivanti_finding_archives(current_state);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_archive_transitions (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
archive_id INTEGER NOT NULL REFERENCES ivanti_finding_archives(id),
|
|
|
|
|
from_state TEXT NOT NULL,
|
|
|
|
|
to_state TEXT NOT NULL,
|
|
|
|
|
severity_at_transition NUMERIC(4,2) NOT NULL DEFAULT 0,
|
|
|
|
|
reason TEXT NOT NULL DEFAULT '',
|
|
|
|
|
transitioned_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_transition_archive_id ON ivanti_archive_transitions(archive_id);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_sync_anomaly_log (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
sync_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
open_count_delta INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
closed_count_delta INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
newly_archived_count INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
returned_count INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
classification_json TEXT NOT NULL DEFAULT '{}',
|
|
|
|
|
return_classification_json TEXT NOT NULL DEFAULT '{}',
|
|
|
|
|
is_significant BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_anomaly_sync_timestamp ON ivanti_sync_anomaly_log(sync_timestamp);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS ivanti_finding_bu_history (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
finding_id TEXT NOT NULL,
|
|
|
|
|
finding_title TEXT NOT NULL DEFAULT '',
|
|
|
|
|
host_name TEXT NOT NULL DEFAULT '',
|
|
|
|
|
previous_bu TEXT NOT NULL,
|
|
|
|
|
new_bu TEXT NOT NULL,
|
|
|
|
|
detected_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bu_history_finding_id ON ivanti_finding_bu_history(finding_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_bu_history_detected_at ON ivanti_finding_bu_history(detected_at);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Atlas action plans cache
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS atlas_action_plans_cache (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
host_id INTEGER NOT NULL UNIQUE,
|
|
|
|
|
has_action_plan BOOLEAN NOT NULL DEFAULT FALSE,
|
|
|
|
|
plan_count INTEGER NOT NULL DEFAULT 0,
|
|
|
|
|
plans_json TEXT NOT NULL DEFAULT '[]',
|
|
|
|
|
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_atlas_cache_host_id ON atlas_action_plans_cache(host_id);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Compliance (NTS AEO) tracking
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS compliance_uploads (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
filename TEXT NOT NULL,
|
|
|
|
|
report_date TEXT,
|
|
|
|
|
uploaded_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
|
|
|
uploaded_at TIMESTAMPTZ DEFAULT NOW(),
|
|
|
|
|
new_count INTEGER DEFAULT 0,
|
|
|
|
|
resolved_count INTEGER DEFAULT 0,
|
|
|
|
|
recurring_count INTEGER DEFAULT 0,
|
|
|
|
|
summary_json TEXT
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS compliance_items (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
upload_id INTEGER NOT NULL REFERENCES compliance_uploads(id) ON DELETE CASCADE,
|
|
|
|
|
hostname TEXT NOT NULL,
|
|
|
|
|
ip_address TEXT,
|
|
|
|
|
device_type TEXT,
|
|
|
|
|
team TEXT,
|
|
|
|
|
metric_id TEXT NOT NULL,
|
|
|
|
|
metric_desc TEXT,
|
|
|
|
|
category TEXT,
|
|
|
|
|
extra_json TEXT,
|
|
|
|
|
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'resolved')),
|
|
|
|
|
first_seen_upload_id INTEGER REFERENCES compliance_uploads(id) ON DELETE SET NULL,
|
|
|
|
|
resolved_upload_id INTEGER REFERENCES compliance_uploads(id) ON DELETE SET NULL,
|
|
|
|
|
seen_count INTEGER DEFAULT 1,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_items_upload ON compliance_items(upload_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_items_identity ON compliance_items(hostname, metric_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_items_team_status ON compliance_items(team, status);
|
|
|
|
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS compliance_notes (
|
|
|
|
|
id SERIAL PRIMARY KEY,
|
|
|
|
|
hostname TEXT NOT NULL,
|
|
|
|
|
metric_id TEXT NOT NULL,
|
|
|
|
|
note TEXT NOT NULL,
|
|
|
|
|
group_id TEXT,
|
|
|
|
|
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
|
|
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_notes_identity ON compliance_notes(hostname, metric_id);
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_compliance_notes_group ON compliance_notes(group_id);
|
|
|
|
|
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
-- Seed data
|
|
|
|
|
-- =============================================================================
|
|
|
|
|
|
|
|
|
|
-- Required documents (idempotent via unique constraint on vendor + document_type)
|
|
|
|
|
INSERT INTO required_documents (vendor, document_type, is_mandatory, description) VALUES
|
|
|
|
|
('Microsoft', 'advisory', TRUE, 'Official Microsoft Security Advisory'),
|
|
|
|
|
('Microsoft', 'screenshot', FALSE, 'Proof of patch application'),
|
|
|
|
|
('Cisco', 'advisory', TRUE, 'Cisco Security Advisory'),
|
|
|
|
|
('Oracle', 'advisory', TRUE, 'Oracle Security Alert'),
|
|
|
|
|
('VMware', 'advisory', TRUE, 'VMware Security Advisory'),
|
|
|
|
|
('Adobe', 'advisory', TRUE, 'Adobe Security Bulletin')
|
|
|
|
|
ON CONFLICT (vendor, document_type) DO NOTHING;
|
|
|
|
|
|
|
|
|
|
-- Ivanti sync state — ensure single row exists
|
|
|
|
|
INSERT INTO ivanti_sync_state (id, total, workflows_json, sync_status)
|
|
|
|
|
VALUES (1, 0, '[]', 'never')
|
|
|
|
|
ON CONFLICT (id) DO NOTHING;
|
|
|
|
|
|
|
|
|
|
-- Ivanti counts cache — ensure single row exists
|
|
|
|
|
INSERT INTO ivanti_counts_cache (id, open_count, closed_count, fp_workflow_counts_json, fp_id_counts_json)
|
|
|
|
|
VALUES (1, 0, 0, '{}', '{}')
|
|
|
|
|
ON CONFLICT (id) DO NOTHING;
|