-- enable foreign keys
PRAGMA foreign_keys = true;

-- set database version
PRAGMA user_version = 314159;

-- create cpes table
CREATE TABLE cpes (
  -- cpe unique ID
  cpe_id        INTEGER PRIMARY KEY,

  -- cpe uri
  cpe_uri       TEXT UNIQUE NOT NULL CHECK (
                  LENGTH(cpe_uri) > 5 AND
                  cpe_uri LIKE 'cpe:/%'
                ),

  -- cpe 2.3 formatting string
  cpe23         TEXT UNIQUE NOT NULL CHECK (
                  LENGTH(cpe23) > 28 AND
                  cpe23 LIKE 'cpe:2.3:%'
                )
);

-- create all fts table (cross join across all
-- titles and references, populated by triggers)
CREATE VIRTUAL TABLE cpe_fts_all USING fts5(
  -- parent CPE id
  cpe_id UNINDEXED,

  -- title
  title,

  -- reference URL
  href,

  -- reference name
  val,

  -- use porter stemming
  tokenize = 'porter'
);

-- create titles table
CREATE TABLE cpe_titles (
  -- title primary key
  cpe_title_id  INTEGER PRIMARY KEY,

  -- external CPE id
  cpe_id        INT NOT NULL
                REFERENCES cpes(cpe_id),

  -- language code
  lang          TEXT NOT NULL
                CHECK (LENGTH(lang) > 0),

  -- text value
  val           TEXT NOT NULL
                CHECK (LENGTH(val) > 0),

  -- prevent duplicate titles of the same name
  UNIQUE (cpe_id, lang)
);

-- create titles fts table
CREATE VIRTUAL TABLE cpe_fts_titles USING fts5(
  -- parent CPE id
  cpe_id UNINDEXED,

  -- title
  title,

  -- use porter stemming
  tokenize = 'porter'
);

-- create titles all fts insert trigger
CREATE TRIGGER tr_cpe_titles_insert_fts_all AFTER INSERT ON cpe_titles
  FOR EACH ROW WHEN (NEW.lang = 'en-US') BEGIN
    INSERT INTO cpe_fts_all(cpe_id, title, href, val)
      SELECT NEW.cpe_id,
             NEW.val,
             b.href,
             b.val

        FROM cpes a
        JOIN cpe_refs b
          ON (b.cpe_id = a.cpe_id)

       WHERE a.cpe_id = NEW.cpe_id;
  END;

-- create titles delete all fts trigger
CREATE TRIGGER tr_cpe_titles_delete_fts_all BEFORE DELETE ON cpe_titles
  FOR EACH ROW WHEN (OLD.lang = 'en-US') BEGIN
    DELETE FROM cpe_fts_all
     WHERE cpe_id = OLD.cpe_id
       AND title = OLD.val;
  END;

-- create titles all fts insert trigger
CREATE TRIGGER tr_cpe_titles_insert_fts_titles AFTER INSERT ON cpe_titles
  FOR EACH ROW WHEN (NEW.lang = 'en-US') BEGIN
    INSERT INTO cpe_fts_titles(cpe_id, title) VALUES (NEW.cpe_id, NEW.val);
  END;

-- create titles delete titles fts trigger
CREATE TRIGGER tr_cpe_titles_delete_fts_titles BEFORE DELETE ON cpe_titles
  FOR EACH ROW WHEN (OLD.lang = 'en-US') BEGIN
    DELETE FROM cpe_fts_titles
     WHERE cpe_id = OLD.cpe_id
       AND title = OLD.val;
  END;

-- create refs table
CREATE TABLE cpe_refs (
  -- reference unique id
  cpe_ref_id    INTEGER PRIMARY KEY,

  -- external CPE id
  cpe_id        INT NOT NULL
                REFERENCES cpes(cpe_id),

  -- url
  href          TEXT NOT NULL CHECK (
                  LENGTH(href) > 0 AND
                  (href LIKE 'http://%' OR href LIKE 'https://%')
                ),

  -- text description
  val           TEXT NOT NULL
                CHECK (LENGTH(val) > 0)
);

-- create refs fts table
CREATE VIRTUAL TABLE cpe_fts_refs USING fts5(
  -- parent CPE id
  cpe_id UNINDEXED,

  -- reference URL
  href,

  -- reference name
  val,

  -- use porter stemming
  tokenize = 'porter'
);

-- create refs insert trigger for all fts
CREATE TRIGGER tr_cpe_refs_insert_fts_all AFTER INSERT ON cpe_refs
  FOR EACH ROW BEGIN
    INSERT INTO cpe_fts_all(cpe_id, title, href, val)
      SELECT NEW.cpe_id,
             b.val,
             NEW.href,
             NEW.val

        FROM cpes a
        JOIN cpe_titles b
          ON (b.cpe_id = a.cpe_id)

       WHERE a.cpe_id = NEW.cpe_id
         AND b.lang = 'en-US';
  END;

-- create refs delete trigger for all fts
CREATE TRIGGER tr_cpe_refs_delete_fts_all BEFORE DELETE ON cpe_refs
  FOR EACH ROW BEGIN
    DELETE FROM cpe_fts_all
     WHERE cpe_id = OLD.cpe_id
       AND href = OLD.lang
       AND val = OLD.val;
  END;

-- create refs insert trigger for refs fts
CREATE TRIGGER tr_cpe_refs_insert_fts_refs AFTER INSERT ON cpe_refs
  FOR EACH ROW BEGIN
    INSERT INTO cpe_fts_refs(cpe_id, href, val) VALUES (NEW.cpe_id, NEW.href, NEW.val);
  END;

-- create refs delete trigger for refs fts
CREATE TRIGGER tr_cpe_refs_delete_fts_refs BEFORE DELETE ON cpe_refs
  FOR EACH ROW BEGIN
    DELETE FROM cpe_fts_refs
     WHERE cpe_id = OLD.cpe_id
       AND href = OLD.lang
       AND val = OLD.val;
  END;

--
-- cpe matches table
--
CREATE TABLE cpe_matches (
  -- cpe match unique ID
  cpe_match_id  INTEGER PRIMARY KEY,

  -- modern CPE 2.3 formatted string
  cpe23         TEXT UNIQUE NOT NULL CHECK (
                  LENGTH(cpe23) > 28 AND
                  cpe23 LIKE 'cpe:2.3:%'
                ),

  -- old CPE 2.2 URI
  cpe22_uri     TEXT NOT NULL
);

-- cpe match vulnerabilities
CREATE TABLE cpe_match_vulnerables (
  cpe_match_id  INTEGER PRIMARY KEY,

  -- true if this match is vulnerable, and false
  -- otherwise
  is_vulnerable BOOLEAN NOT NULL,

  -- add foreign key constraint
  FOREIGN KEY (cpe_match_id)
    REFERENCES cpe_matches(cpe_match_id)
);

--
-- cpe match version minimums (e.g. version starts)
--
CREATE TABLE cpe_match_version_mins (
  cpe_match_id  INTEGER PRIMARY KEY,

  -- valid values
  --
  -- * true: boundary includes this version (e.g. it was specified by
  --   versionStartIncluding)
  -- * false: boundary excludes this version (e.g., it was specified by
  --   versionStartExcluding)
  is_inclusive  BOOLEAN NOT NULL,

  -- minimum version
  min_version   TEXT NOT NULL
                CHECK (LENGTH(min_version) > 0),

  -- add foreign key constraint
  FOREIGN KEY (cpe_match_id)
    REFERENCES cpe_matches(cpe_match_id)
);

--
-- cpe match version maximums (e.g. version ends)
--
CREATE TABLE cpe_match_version_maxs (
  cpe_match_id  INTEGER PRIMARY KEY,

  -- valid values
  --
  -- * true: boundary includes this version (e.g. it was specified by
  --   versionEndIncluding)
  -- * false: boundary excludes this version (e.g., it was specified by
  --   versionEndExcluding)
  is_inclusive  BOOLEAN NOT NULL,

  -- maximum version
  max_version   TEXT NOT NULL
                CHECK (LENGTH(max_version) > 0),

  -- add foreign key constraint
  FOREIGN KEY (cpe_match_id)
    REFERENCES cpe_matches(cpe_match_id)
);

-- map of matching
CREATE TABLE cpe_match_cpes (
  -- CPE match ID
  cpe_match_id  INTEGER NOT NULL
                REFERENCES cpe_matches(cpe_match_id),

  -- CPE ID
  cpe_id        INTEGER NOT NULL
                REFERENCES cpes(cpe_id),

  -- nullable cpe22 URI
  -- normally we would want this in a separate table, but
  -- we don't care that much about this value
  cpe22_uri     TEXT,

  PRIMARY KEY (cpe_match_id, cpe_id)
);

--
-- Descriptions of CVEs and CVE problem types.
--
CREATE TABLE descriptions (
  -- description unique ID
  desc_id       INTEGER PRIMARY KEY,

  -- description language
  lang          TEXT NOT NULL CHECK (
                  -- FIXME: not sure about maximum value here
                  LENGTH(lang) BETWEEN 1 AND 10
                ),

  -- description text
  value         TEXT NOT NULL CHECK (
                  LENGTH(value) > 0
                )
);

--
-- CVE feeds.
--
CREATE TABLE feeds (
  -- feed unique ID
  feed_id       INTEGER PRIMARY KEY,

  -- time that feed was added
  created_at    TIMESTAMP WITH TIME ZONE NOT NULL
                DEFAULT CURRENT_TIMESTAMP,

  -- data timestamp
  data_time     TIMESTAMP WITH TIME ZONE NOT NULL
);

--
-- CVE feed items.
--
CREATE TABLE feed_items (
  item_id       INTEGER PRIMARY KEY,

  -- feed that this CVE belongs to
  feed_id       INTEGER NOT NULL
                REFERENCES feeds(feed_id),

  -- published timestamp
  published_at  TIMESTAMP WITH TIME ZONE NOT NULL,

  -- last modified timestamp
  modified_at   TIMESTAMP WITH TIME ZONE NOT NULL
);

-- create index
CREATE INDEX in_feed_items_feed_id ON feed_items(feed_id);

--
-- CVEs
--
-- NOTE: this combines the concept of an "item" and a "cve" from the
-- source feed.  maybe we should split them up again?
--
CREATE TABLE cves (
  -- CVE unique ID
  cve_id        INTEGER PRIMARY KEY,

  -- feed item that this CVE belongs to
  item_id       INTEGER NOT NULL
                REFERENCES feed_items(item_id),

  -- NVD CVE ID (e.g. "CVE-YYYY-XXXX)
  nvd_cve_id    TEXT NOT NULL CHECK (
                  LENGTH(nvd_cve_id) BETWEEN 9 AND 17 AND
                  nvd_cve_id LIKE 'CVE-%'
                ),

  -- assigner, or empty string
  assigner      TEXT NOT NULL DEFAULT ''
);

-- create index
CREATE INDEX in_cves_item_id ON cves(item_id);

--
-- CVE descriptions
--
CREATE TABLE cve_descriptions (
  -- CVE unique ID
  cve_id        INTEGER NOT NULL
                REFERENCES cves(cve_id),

  -- description unique ID
  desc_id       INTEGER NOT NULL
                REFERENCES descriptions(desc_id),

  PRIMARY KEY (cve_id, desc_id)
);

-- create index
CREATE INDEX in_cve_descriptions_cve_id ON cve_descriptions(cve_id);

--
-- CVE problem types
--
CREATE TABLE cve_problems (
  -- problem type unique ID
  pt_id         INTEGER PRIMARY KEY,

  -- CVE that this problem type belongs to
  cve_id        INTEGER NOT NULL
                REFERENCES cves(cve_id)
);

-- create index
CREATE INDEX in_cve_problems_cve_id ON cve_problems(cve_id);

--
-- CVE problem type descriptions
--
CREATE TABLE cve_problem_descriptions (
  -- problem type that this description belongs to
  pt_id         INTEGER NOT NULL
                REFERENCES cve_problems(pt_id),

  -- description unique ID
  desc_id       INTEGER NOT NULL
                REFERENCES descriptions(desc_id),

  PRIMARY KEY (pt_id, desc_id)
);

--
-- CVE references
--
CREATE TABLE cve_refs (
  -- cve reference unique ID
  cve_ref_id    INTEGER PRIMARY KEY,

  -- CVE unique ID
  cve_id        INTEGER NOT NULL
                REFERENCES cves(cve_id),

  -- reference URL
  url           TEXT NOT NULL CHECK (
                  -- FIXME: not sure about length here
                  LENGTH(url) BETWEEN 7 AND 1024
                ),

  -- reference name
  name          TEXT NOT NULL CHECK (
                  -- FIXME: not sure about length here
                  LENGTH(name) BETWEEN 1 AND 1024
                ),

  -- reference name
  source        TEXT NOT NULL
);

-- create index
CREATE INDEX in_cve_refs_cve_id ON cve_refs(cve_id);

--
-- cve reference tags
--
CREATE TABLE cve_ref_tags (
  cve_ref_id    INTEGER NOT NULL
                REFERENCES cve_refs(cve_ref_id),

  -- tag
  tag           TEXT NOT NULL CHECK (
                  -- FIXME: not sure about maximum length
                  LENGTH(tag) BETWEEN 1 AND 128
                ),

  -- prevent duplicate tags
  UNIQUE (cve_ref_id, tag)
);

-- create index
CREATE INDEX in_cve_ref_tags_cve_ref_id ON cve_ref_tags(cve_ref_id);
