xpl-intel 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. xpl_intel-1.2.0/LICENSE +123 -0
  2. xpl_intel-1.2.0/PKG-INFO +24 -0
  3. xpl_intel-1.2.0/README.md +55 -0
  4. xpl_intel-1.2.0/ingestion/__init__.py +2 -0
  5. xpl_intel-1.2.0/ingestion/cli.py +162 -0
  6. xpl_intel-1.2.0/ingestion/dedup.py +49 -0
  7. xpl_intel-1.2.0/ingestion/output/__init__.py +9 -0
  8. xpl_intel-1.2.0/ingestion/output/csv_writer.py +36 -0
  9. xpl_intel-1.2.0/ingestion/output/json_writer.py +17 -0
  10. xpl_intel-1.2.0/ingestion/output/sqlite_writer.py +116 -0
  11. xpl_intel-1.2.0/ingestion/sources/__init__.py +0 -0
  12. xpl_intel-1.2.0/ingestion/sources/cisa_kev.py +44 -0
  13. xpl_intel-1.2.0/ingestion/sources/epss_api.py +49 -0
  14. xpl_intel-1.2.0/ingestion/sources/exploitdb.py +50 -0
  15. xpl_intel-1.2.0/ingestion/sources/github_advisory.py +113 -0
  16. xpl_intel-1.2.0/ingestion/sources/nuclei_templates.py +62 -0
  17. xpl_intel-1.2.0/ingestion/sources/nvd_api.py +99 -0
  18. xpl_intel-1.2.0/ingestion/sources/packetstorm_rss.py +47 -0
  19. xpl_intel-1.2.0/ingestion/sources/vulncheck_kev.py +67 -0
  20. xpl_intel-1.2.0/ingestion/sources/watchtowr_rss.py +48 -0
  21. xpl_intel-1.2.0/pyproject.toml +83 -0
  22. xpl_intel-1.2.0/schemas/__init__.py +0 -0
  23. xpl_intel-1.2.0/schemas/advisory_v1.py +73 -0
  24. xpl_intel-1.2.0/schemas/py.typed +0 -0
  25. xpl_intel-1.2.0/setup.cfg +4 -0
  26. xpl_intel-1.2.0/tests/test_advisory.py +134 -0
  27. xpl_intel-1.2.0/tests/test_cli.py +110 -0
  28. xpl_intel-1.2.0/tests/test_core.py +103 -0
  29. xpl_intel-1.2.0/tests/test_dedup.py +115 -0
  30. xpl_intel-1.2.0/tests/test_epss.py +75 -0
  31. xpl_intel-1.2.0/tests/test_exploitdb.py +77 -0
  32. xpl_intel-1.2.0/tests/test_ghsa.py +156 -0
  33. xpl_intel-1.2.0/tests/test_kev.py +90 -0
  34. xpl_intel-1.2.0/tests/test_nuclei.py +123 -0
  35. xpl_intel-1.2.0/tests/test_nvd.py +197 -0
  36. xpl_intel-1.2.0/tests/test_output.py +186 -0
  37. xpl_intel-1.2.0/tests/test_packetstorm.py +68 -0
  38. xpl_intel-1.2.0/tests/test_vulncheck.py +105 -0
  39. xpl_intel-1.2.0/tests/test_watchtowr.py +101 -0
  40. xpl_intel-1.2.0/xpl_intel.egg-info/PKG-INFO +24 -0
  41. xpl_intel-1.2.0/xpl_intel.egg-info/SOURCES.txt +43 -0
  42. xpl_intel-1.2.0/xpl_intel.egg-info/dependency_links.txt +1 -0
  43. xpl_intel-1.2.0/xpl_intel.egg-info/entry_points.txt +3 -0
  44. xpl_intel-1.2.0/xpl_intel.egg-info/requires.txt +11 -0
  45. xpl_intel-1.2.0/xpl_intel.egg-info/top_level.txt +2 -0
@@ -0,0 +1,123 @@
1
+ # XPL-Suite Security Research License (XSRL) v1.0
2
+
3
+ Copyright (c) 2024-2026 Andre Henrike de Oliveira (@mrhenrike) / Uniao Geek
4
+ All rights reserved.
5
+
6
+ ## 1. Definitions
7
+
8
+ - "Software" means the source code, documentation, binaries, scripts, configuration
9
+ files, and any other materials included in this repository.
10
+ - "Author" means the original copyright holder: Andre Henrike de Oliveira (@mrhenrike).
11
+ - "Original Repository" means the source repository hosted at
12
+ https://github.com/mrhenrike/xpl-intel from which the Software originates.
13
+ - "Upstream Credits" means the authors and licenses of third-party components,
14
+ libraries, or code referenced in the Software, as documented in source file headers,
15
+ CREDITS files, or README attribution sections.
16
+ - "Authorized Security Testing" means penetration testing, vulnerability assessment,
17
+ red team exercises, purple team exercises, security auditing, or similar activities
18
+ conducted with explicit written permission from the owner of the target system(s).
19
+
20
+ ## 2. Grant of Rights
21
+
22
+ Subject to the terms and conditions of this License, the Author grants you a
23
+ non-exclusive, worldwide, royalty-free license to:
24
+
25
+ a) **Use** the Software for educational purposes, academic research, security studies,
26
+ and Authorized Security Testing;
27
+ b) **Copy and reproduce** the Software;
28
+ c) **Modify** the Software and create derivative works;
29
+ d) **Fork** the Software and maintain independent copies;
30
+ e) **Distribute** the Software and derivative works, provided all conditions in
31
+ Section 3 are met.
32
+
33
+ ## 3. Conditions
34
+
35
+ Any use, copy, modification, fork, or distribution of the Software must comply with
36
+ ALL of the following conditions:
37
+
38
+ ### 3.1 Attribution
39
+
40
+ a) You must retain the Author's original copyright notice and attribution in all
41
+ copies and derivative works.
42
+ b) You must retain all Upstream Credits, including but not limited to: original author
43
+ names, license notices, and attribution statements of third-party code used as a
44
+ base or reference for the Software.
45
+ c) Modified files must carry a prominent notice stating that you changed the files and
46
+ the date of any change.
47
+ d) Derivative works must include a clear statement identifying the original Software
48
+ and its Author.
49
+ e) Any copy, fork, or derivative work must include a visible reference to the Original
50
+ Repository URL (https://github.com/mrhenrike/xpl-intel) in the project's
51
+ README or equivalent documentation, so that users can trace the Software back to
52
+ its source.
53
+ f) The Author's name and credits must not be removed, obscured, or replaced in any
54
+ copy, fork, or derivative work. The Author must remain credited as the original
55
+ creator in all circumstances.
56
+
57
+ ### 3.2 Same License
58
+
59
+ a) Any distribution of the Software or derivative works must be under this same
60
+ License (XSRL v1.0).
61
+ b) You may not apply any additional restrictions or alternative license terms to the
62
+ Software or derivative works.
63
+ c) You may not sublicense the Software under different terms.
64
+
65
+ ### 3.3 Non-Commercial Use Only
66
+
67
+ a) The Software may not be used, in whole or in part, for commercial purposes.
68
+ b) "Commercial purposes" includes but is not limited to: selling the Software or
69
+ derivative works, incorporating the Software into commercial products or services,
70
+ using the Software to generate revenue directly or indirectly, and offering the
71
+ Software as part of a paid service.
72
+ c) Use in non-profit organizations, academic institutions, and personal projects is
73
+ permitted, provided no commercial gain is derived.
74
+
75
+ ### 3.4 Ethical Use Only
76
+
77
+ a) The Software may only be used for:
78
+ - Educational purposes and academic research;
79
+ - Security studies, training, and skill development;
80
+ - Authorized Security Testing (as defined in Section 1);
81
+ - Capture The Flag (CTF) competitions and security labs;
82
+ - Defensive security, incident response, and threat analysis.
83
+
84
+ b) The Software may NOT be used for:
85
+ - Unauthorized access to systems, networks, or data;
86
+ - Any activity that violates applicable laws or regulations;
87
+ - Malicious purposes, including but not limited to: deploying malware against
88
+ unauthorized targets, conducting denial-of-service attacks, unauthorized
89
+ surveillance, data theft, or any form of cybercrime;
90
+ - Targeting individuals, organizations, or infrastructure without explicit
91
+ authorization;
92
+ - Any purpose that causes harm to individuals or organizations.
93
+
94
+ ## 4. No Warranty
95
+
96
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
97
+ INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
98
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
99
+ ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
100
+ OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
101
+ OTHER DEALINGS IN THE SOFTWARE.
102
+
103
+ ## 5. Limitation of Liability
104
+
105
+ The Author shall not be held responsible for any misuse of the Software. Users are
106
+ solely responsible for ensuring their use of the Software complies with all applicable
107
+ laws, regulations, and ethical standards.
108
+
109
+ ## 6. Termination
110
+
111
+ This License and the rights granted hereunder will terminate automatically if you fail
112
+ to comply with any of the terms and conditions of this License. Upon termination, you
113
+ must cease all use and destroy all copies of the Software in your possession.
114
+
115
+ ## 7. Severability
116
+
117
+ If any provision of this License is held to be unenforceable, such provision shall be
118
+ reformed only to the extent necessary to make it enforceable, and the remaining
119
+ provisions shall continue in full force and effect.
120
+
121
+ ---
122
+
123
+ SPDX-License-Identifier: LicenseRef-XSRL-1.0
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: xpl-intel
3
+ Version: 1.2.0
4
+ Summary: XPL-Intel — CVE ingestion pipeline for XPL-Suite. CISA KEV, NVD, watchTowr, Nuclei, ExploitDB.
5
+ Author-email: André Henrique <henrique.santos@uniaogeek.com.br>
6
+ License: XSRL-1.0
7
+ Keywords: cve,vulnerability,intel,kev,nvd,nuclei,xpl-suite
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Security
12
+ Requires-Python: >=3.9
13
+ License-File: LICENSE
14
+ Requires-Dist: requests<3.0,>=2.31.0
15
+ Requires-Dist: rich>=13.0
16
+ Requires-Dist: defusedxml>=0.7.1
17
+ Requires-Dist: argcomplete>=3.0
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest>=7.0; extra == "dev"
20
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
21
+ Requires-Dist: ruff>=0.4; extra == "dev"
22
+ Requires-Dist: mypy>=1.10; extra == "dev"
23
+ Requires-Dist: types-requests>=2.31; extra == "dev"
24
+ Dynamic: license-file
@@ -0,0 +1,55 @@
1
+ # xpl-intel
2
+
3
+ CVE ingestion and threat intelligence pipeline for [XPL-Suite](https://github.com/mrhenrike).
4
+
5
+ ## Overview
6
+
7
+ `xpl-intel` aggregates vulnerability data from multiple authoritative sources into a unified advisory format:
8
+
9
+ ### Ingestion Sources
10
+ - **CISA KEV** — Known Exploited Vulnerabilities catalog
11
+ - **NVD API** — NIST National Vulnerability Database
12
+ - **VulnCheck KEV** — VulnCheck enriched KEV data
13
+ - **ExploitDB** — Exploit Database (CSV mirror)
14
+ - **GitHub Advisory (GHSA)** — GitHub Security Advisories
15
+ - **Nuclei Templates** — ProjectDiscovery Nuclei CVE templates
16
+ - **watchTowr RSS** — watchTowr Labs advisory feed
17
+ - **PacketStorm RSS** — PacketStorm Security exploit feed (NEW)
18
+ - **EPSS API** — FIRST Exploit Prediction Scoring System enrichment (NEW)
19
+
20
+ ### Output Formats
21
+ - JSON (structured advisory records)
22
+ - CSV (tabular export)
23
+ - SQLite (queryable local database)
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ pip install xpl-intel
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```bash
34
+ # Ingest from all sources
35
+ xpl-intel ingest --all
36
+
37
+ # Ingest from specific source
38
+ xpl-intel ingest --source cisa_kev
39
+
40
+ # Output to SQLite
41
+ xpl-intel ingest --all --output sqlite --db vulns.db
42
+ ```
43
+
44
+ ## Requirements
45
+
46
+ - Python >= 3.9
47
+ - `requests`, `rich`, `defusedxml`, `argcomplete`
48
+
49
+ ## License
50
+
51
+ XSRL-1.0. See [LICENSE](LICENSE).
52
+
53
+ ## Author
54
+
55
+ André Henrique ([@mrhenrike](https://github.com/mrhenrike)) | União Geek
@@ -0,0 +1,2 @@
1
+ __version__ = "1.2.0"
2
+ PRODUCT = "XPL-Intel"
@@ -0,0 +1,162 @@
1
+ """XPL-Intel CLI — CVE ingestion pipeline."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+ from datetime import datetime, timezone
9
+
10
+ import argcomplete
11
+
12
+ from ingestion import PRODUCT, __version__
13
+ from schemas.advisory_v1 import Advisory
14
+
15
+ ALL_SOURCES = ["kev", "nvd", "watchtowr", "nuclei", "exploitdb", "vulncheck", "ghsa"]
16
+ OUTPUT_FORMATS = ["json", "sqlite", "csv"]
17
+
18
+
19
+ def main(argv: list[str] | None = None) -> int:
20
+ parser = argparse.ArgumentParser(
21
+ prog="xpl-intel",
22
+ description=f"{PRODUCT} v{__version__} — CVE ingestion pipeline",
23
+ )
24
+ parser.add_argument("--version", action="version", version=f"{PRODUCT} {__version__}")
25
+
26
+ sub = parser.add_subparsers(dest="command")
27
+
28
+ ingest_p = sub.add_parser("ingest", help="Run ingestion from sources")
29
+ ingest_p.add_argument("--sources", "-s", nargs="+",
30
+ choices=[*ALL_SOURCES, "all"],
31
+ default=["all"])
32
+ ingest_p.add_argument("--nvd-key", help="NVD API key (optional)")
33
+ ingest_p.add_argument("--nvd-days", type=int, default=7, help="NVD lookback days")
34
+ ingest_p.add_argument("--nuclei-dir", default="nuclei-templates", help="Nuclei templates dir")
35
+ ingest_p.add_argument("--exploitdb-csv", help="ExploitDB CSV path")
36
+ ingest_p.add_argument("--vulncheck-token", help="VulnCheck API token")
37
+ ingest_p.add_argument("--github-token", help="GitHub token for GHSA")
38
+ ingest_p.add_argument("--ghsa-severity", default="critical,high",
39
+ help="GHSA severity filter (default: critical,high)")
40
+ ingest_p.add_argument("--ghsa-days", type=int, default=7, help="GHSA lookback days")
41
+ ingest_p.add_argument("--output", "-o", default="output", help="Output directory")
42
+ ingest_p.add_argument("--format", "-f", nargs="+",
43
+ choices=OUTPUT_FORMATS, default=["json"],
44
+ help="Output formats (default: json)")
45
+
46
+ sub.add_parser("sources", help="List available sources")
47
+
48
+ argcomplete.autocomplete(parser)
49
+ args = parser.parse_args(argv)
50
+ if not args.command:
51
+ parser.print_help()
52
+ return 0
53
+
54
+ if args.command == "sources":
55
+ _print_sources()
56
+ return 0
57
+
58
+ return _run_ingest(args)
59
+
60
+
61
+ def _print_sources() -> None:
62
+ sources = [
63
+ ("kev", "CISA KEV catalog (CSV daily)", "https://www.cisa.gov/known-exploited-vulnerabilities-catalog"),
64
+ ("nvd", "NVD API v2 (delta weekly)", "https://nvd.nist.gov/"),
65
+ ("watchtowr", "watchTowr Labs RSS", "https://labs.watchtowr.com/"),
66
+ ("nuclei", "Nuclei templates (local)", "https://github.com/projectdiscovery/nuclei-templates"),
67
+ ("exploitdb", "ExploitDB CSV (searchsploit)", "https://www.exploit-db.com/"),
68
+ ("vulncheck", "VulnCheck KEV enriched", "https://vulncheck.com/"),
69
+ ("ghsa", "GitHub Advisory Database", "https://github.com/advisories"),
70
+ ]
71
+ print(f"{PRODUCT} v{__version__} — Available sources:\n")
72
+ for name, desc, url in sources:
73
+ print(f" {name:12s} {desc}")
74
+ print(f" {'':12s} {url}")
75
+ print()
76
+
77
+
78
+ def _run_ingest(args: argparse.Namespace) -> int:
79
+ from ingestion.dedup import deduplicate
80
+
81
+ sources_to_run: list[str] = args.sources
82
+ if "all" in sources_to_run:
83
+ sources_to_run = list(ALL_SOURCES)
84
+
85
+ all_advisories: list[Advisory] = []
86
+
87
+ for source in sources_to_run:
88
+ try:
89
+ advisories = _fetch_source(source, args)
90
+ print(f"[+] {source}: {len(advisories)} advisories")
91
+ all_advisories.extend(advisories)
92
+ except Exception as exc:
93
+ print(f"[-] {source}: {exc}", file=sys.stderr)
94
+
95
+ if not all_advisories:
96
+ print("[*] No advisories ingested")
97
+ return 0
98
+
99
+ deduped = deduplicate(all_advisories)
100
+ print(f"[+] Deduplicated: {len(all_advisories)} → {len(deduped)}")
101
+
102
+ os.makedirs(args.output, exist_ok=True)
103
+ ts = datetime.now(timezone.utc).strftime("%Y-%m-%d")
104
+
105
+ formats: list[str] = args.format
106
+ for fmt in formats:
107
+ _write_output(deduped, args.output, ts, fmt)
108
+
109
+ return 0
110
+
111
+
112
+ def _write_output(advisories: list[Advisory], output_dir: str, ts: str, fmt: str) -> None:
113
+ if fmt == "json":
114
+ from ingestion.output.json_writer import write_json
115
+ path = write_json(advisories, os.path.join(output_dir, f"advisories-{ts}.json"))
116
+ elif fmt == "sqlite":
117
+ from ingestion.output.sqlite_writer import write_sqlite
118
+ path = write_sqlite(advisories, os.path.join(output_dir, f"advisories-{ts}.db"))
119
+ elif fmt == "csv":
120
+ from ingestion.output.csv_writer import write_csv
121
+ path = write_csv(advisories, os.path.join(output_dir, f"advisories-{ts}.csv"))
122
+ else:
123
+ print(f"[-] Unknown format: {fmt}", file=sys.stderr)
124
+ return
125
+ print(f"[+] Output ({fmt}): {path} ({len(advisories)} advisories)")
126
+
127
+
128
+ def _fetch_source(source: str, args: argparse.Namespace) -> list[Advisory]:
129
+ if source == "kev":
130
+ from ingestion.sources.cisa_kev import fetch as fetch_kev
131
+ return fetch_kev()
132
+ if source == "nvd":
133
+ from ingestion.sources.nvd_api import fetch as fetch_nvd
134
+ return fetch_nvd(days_back=args.nvd_days, api_key=args.nvd_key)
135
+ if source == "watchtowr":
136
+ from ingestion.sources.watchtowr_rss import fetch as fetch_watchtowr
137
+ return fetch_watchtowr()
138
+ if source == "nuclei":
139
+ from ingestion.sources.nuclei_templates import fetch as fetch_nuclei
140
+ return fetch_nuclei(templates_dir=args.nuclei_dir)
141
+ if source == "exploitdb":
142
+ from ingestion.sources.exploitdb import fetch as fetch_exploitdb
143
+ return fetch_exploitdb(csv_path=args.exploitdb_csv)
144
+ if source == "vulncheck":
145
+ from ingestion.sources.vulncheck_kev import fetch as fetch_vulncheck
146
+ token = args.vulncheck_token or os.environ.get("VULNCHECK_TOKEN", "")
147
+ if not token:
148
+ raise ValueError("VulnCheck API token required (--vulncheck-token or VULNCHECK_TOKEN env)")
149
+ return fetch_vulncheck(api_token=token)
150
+ if source == "ghsa":
151
+ from ingestion.sources.github_advisory import fetch as fetch_ghsa
152
+ token = args.github_token or os.environ.get("GITHUB_TOKEN")
153
+ return fetch_ghsa(
154
+ days_back=args.ghsa_days,
155
+ github_token=token,
156
+ severity=args.ghsa_severity,
157
+ )
158
+ return []
159
+
160
+
161
+ if __name__ == "__main__":
162
+ sys.exit(main())
@@ -0,0 +1,49 @@
1
+ """Deduplication engine — merge advisories by CVE+CNA, keep richest data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from schemas.advisory_v1 import Advisory
6
+
7
+
8
+ def deduplicate(advisories: list[Advisory]) -> list[Advisory]:
9
+ seen: dict[str, Advisory] = {}
10
+
11
+ for adv in advisories:
12
+ key = adv.cve_id
13
+ if key in seen:
14
+ seen[key] = _merge(seen[key], adv)
15
+ else:
16
+ seen[key] = adv
17
+
18
+ return sorted(seen.values(), key=lambda a: a.cve_id)
19
+
20
+
21
+ def _merge(existing: Advisory, new: Advisory) -> Advisory:
22
+ if not existing.description and new.description:
23
+ existing.description = new.description
24
+ if new.cvss_score is not None and (existing.cvss_score is None or new.cvss_score > existing.cvss_score):
25
+ existing.cvss_score = new.cvss_score
26
+ existing.cvss_vector = new.cvss_vector or existing.cvss_vector
27
+ if new.epss_score is not None:
28
+ existing.epss_score = new.epss_score
29
+ existing.epss_percentile = new.epss_percentile
30
+ if new.kev:
31
+ existing.kev = True
32
+ existing.kev_date_added = new.kev_date_added or existing.kev_date_added
33
+ existing.kev_due_date = new.kev_due_date or existing.kev_due_date
34
+ if not existing.vendor and new.vendor:
35
+ existing.vendor = new.vendor
36
+ if not existing.product and new.product:
37
+ existing.product = new.product
38
+ if new.cwe and not existing.cwe:
39
+ existing.cwe = new.cwe
40
+
41
+ existing.references = list(dict.fromkeys(existing.references + new.references))[:20]
42
+ existing.poc_urls = list(dict.fromkeys(existing.poc_urls + new.poc_urls))[:10]
43
+ existing.tags = list(dict.fromkeys(existing.tags + new.tags))[:20]
44
+ existing.mitre_techniques = list(dict.fromkeys(existing.mitre_techniques + new.mitre_techniques))
45
+
46
+ sources = f"{existing.source},{new.source}"
47
+ existing.source = ",".join(dict.fromkeys(sources.split(",")))
48
+
49
+ return existing
@@ -0,0 +1,9 @@
1
+ """Output writers for xpl-intel advisories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ingestion.output.csv_writer import write_csv
6
+ from ingestion.output.json_writer import write_json
7
+ from ingestion.output.sqlite_writer import write_sqlite
8
+
9
+ __all__ = ["write_csv", "write_json", "write_sqlite"]
@@ -0,0 +1,36 @@
1
+ """CSV output writer for xpl-intel advisories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import json
7
+ from pathlib import Path
8
+
9
+ from schemas.advisory_v1 import Advisory
10
+
11
+ CSV_FIELDS = [
12
+ "cve_id", "title", "description", "cvss_score", "cvss_vector",
13
+ "epss_score", "epss_percentile", "kev", "kev_date_added", "kev_due_date",
14
+ "vendor", "product", "affected_versions", "fixed_versions", "patch_url",
15
+ "poc_urls", "references", "cwe", "source", "source_url",
16
+ "status", "ingested_at", "mitre_techniques", "tags",
17
+ ]
18
+
19
+
20
+ def write_csv(advisories: list[Advisory], path: str | Path) -> Path:
21
+ out = Path(path)
22
+ out.parent.mkdir(parents=True, exist_ok=True)
23
+
24
+ with open(out, "w", newline="", encoding="utf-8") as fh:
25
+ writer = csv.DictWriter(fh, fieldnames=CSV_FIELDS)
26
+ writer.writeheader()
27
+ for adv in advisories:
28
+ row = adv.to_dict()
29
+ row.pop("schema", None)
30
+ for key in ("affected_versions", "fixed_versions", "poc_urls",
31
+ "references", "mitre_techniques", "tags"):
32
+ if isinstance(row.get(key), list):
33
+ row[key] = json.dumps(row[key])
34
+ writer.writerow({k: row.get(k, "") for k in CSV_FIELDS})
35
+
36
+ return out
@@ -0,0 +1,17 @@
1
+ """JSON output writer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from schemas.advisory_v1 import Advisory
10
+
11
+
12
+ def write_json(advisories: list[Advisory], path: str | Path) -> Path:
13
+ out = Path(path)
14
+ data: list[dict[str, Any]] = [a.to_dict() for a in advisories]
15
+ out.parent.mkdir(parents=True, exist_ok=True)
16
+ out.write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
17
+ return out
@@ -0,0 +1,116 @@
1
+ """SQLite output writer for xpl-intel advisories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ from pathlib import Path
8
+
9
+ from schemas.advisory_v1 import Advisory
10
+
11
+ SCHEMA_SQL = """
12
+ CREATE TABLE IF NOT EXISTS advisories (
13
+ cve_id TEXT PRIMARY KEY,
14
+ title TEXT NOT NULL,
15
+ description TEXT DEFAULT '',
16
+ cvss_score REAL,
17
+ cvss_vector TEXT DEFAULT '',
18
+ epss_score REAL,
19
+ epss_percentile REAL,
20
+ kev INTEGER DEFAULT 0,
21
+ kev_date_added TEXT,
22
+ kev_due_date TEXT,
23
+ vendor TEXT DEFAULT '',
24
+ product TEXT DEFAULT '',
25
+ affected_versions TEXT DEFAULT '[]',
26
+ fixed_versions TEXT DEFAULT '[]',
27
+ patch_url TEXT DEFAULT '',
28
+ poc_urls TEXT DEFAULT '[]',
29
+ "references" TEXT DEFAULT '[]',
30
+ cwe TEXT DEFAULT '',
31
+ source TEXT DEFAULT '',
32
+ source_url TEXT DEFAULT '',
33
+ status TEXT DEFAULT 'unverified',
34
+ ingested_at TEXT DEFAULT '',
35
+ mitre_techniques TEXT DEFAULT '[]',
36
+ tags TEXT DEFAULT '[]'
37
+ );
38
+ CREATE INDEX IF NOT EXISTS idx_advisories_vendor ON advisories(vendor);
39
+ CREATE INDEX IF NOT EXISTS idx_advisories_cvss ON advisories(cvss_score);
40
+ CREATE INDEX IF NOT EXISTS idx_advisories_kev ON advisories(kev);
41
+ CREATE INDEX IF NOT EXISTS idx_advisories_source ON advisories(source);
42
+ """
43
+
44
+ UPSERT_SQL = """
45
+ INSERT INTO advisories (
46
+ cve_id, title, description, cvss_score, cvss_vector,
47
+ epss_score, epss_percentile, kev, kev_date_added, kev_due_date,
48
+ vendor, product, affected_versions, fixed_versions, patch_url,
49
+ poc_urls, "references", cwe, source, source_url,
50
+ status, ingested_at, mitre_techniques, tags
51
+ ) VALUES (
52
+ ?, ?, ?, ?, ?,
53
+ ?, ?, ?, ?, ?,
54
+ ?, ?, ?, ?, ?,
55
+ ?, ?, ?, ?, ?,
56
+ ?, ?, ?, ?
57
+ ) ON CONFLICT(cve_id) DO UPDATE SET
58
+ title = CASE WHEN excluded.title != '' THEN excluded.title ELSE advisories.title END,
59
+ description = CASE WHEN excluded.description != '' THEN excluded.description ELSE advisories.description END,
60
+ cvss_score = CASE WHEN excluded.cvss_score IS NOT NULL THEN excluded.cvss_score ELSE advisories.cvss_score END,
61
+ cvss_vector = CASE WHEN excluded.cvss_vector != '' THEN excluded.cvss_vector ELSE advisories.cvss_vector END,
62
+ kev = MAX(advisories.kev, excluded.kev),
63
+ source = advisories.source || ',' || excluded.source,
64
+ ingested_at = excluded.ingested_at
65
+ """
66
+
67
+
68
+ def write_sqlite(advisories: list[Advisory], path: str | Path) -> Path:
69
+ out = Path(path)
70
+ out.parent.mkdir(parents=True, exist_ok=True)
71
+
72
+ conn = sqlite3.connect(str(out))
73
+ try:
74
+ conn.executescript(SCHEMA_SQL)
75
+ rows = [_advisory_to_row(a) for a in advisories]
76
+ conn.executemany(UPSERT_SQL, rows)
77
+ conn.commit()
78
+ finally:
79
+ conn.close()
80
+
81
+ return out
82
+
83
+
84
+ def _advisory_to_row(adv: Advisory) -> tuple[
85
+ str, str, str, float | None, str,
86
+ float | None, float | None, int, str | None, str | None,
87
+ str, str, str, str, str,
88
+ str, str, str, str, str,
89
+ str, str, str, str,
90
+ ]:
91
+ return (
92
+ adv.cve_id,
93
+ adv.title,
94
+ adv.description,
95
+ adv.cvss_score,
96
+ adv.cvss_vector,
97
+ adv.epss_score,
98
+ adv.epss_percentile,
99
+ 1 if adv.kev else 0,
100
+ adv.kev_date_added,
101
+ adv.kev_due_date,
102
+ adv.vendor,
103
+ adv.product,
104
+ json.dumps(adv.affected_versions),
105
+ json.dumps(adv.fixed_versions),
106
+ adv.patch_url,
107
+ json.dumps(adv.poc_urls),
108
+ json.dumps(adv.references),
109
+ adv.cwe,
110
+ adv.source,
111
+ adv.source_url,
112
+ adv.status,
113
+ adv.ingested_at,
114
+ json.dumps(adv.mitre_techniques),
115
+ json.dumps(adv.tags),
116
+ )
File without changes
@@ -0,0 +1,44 @@
1
+ """CISA Known Exploited Vulnerabilities (KEV) catalog ingestion."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import csv
6
+ import io
7
+ from datetime import datetime, timezone
8
+
9
+ import requests
10
+
11
+ from schemas.advisory_v1 import Advisory
12
+
13
+ KEV_URL = "https://www.cisa.gov/sites/default/files/csv/known_exploited_vulnerabilities.csv"
14
+
15
+
16
+ def fetch(timeout: int = 30) -> list[Advisory]:
17
+ resp = requests.get(KEV_URL, timeout=timeout)
18
+ resp.raise_for_status()
19
+
20
+ reader = csv.DictReader(io.StringIO(resp.text))
21
+ advisories = []
22
+ now = datetime.now(timezone.utc).isoformat()
23
+
24
+ for row in reader:
25
+ adv = Advisory(
26
+ cve_id=row.get("cveID", ""),
27
+ title=row.get("vulnerabilityName", ""),
28
+ description=row.get("shortDescription", ""),
29
+ vendor=row.get("vendorProject", ""),
30
+ product=row.get("product", ""),
31
+ kev=True,
32
+ kev_date_added=row.get("dateAdded", ""),
33
+ kev_due_date=row.get("dueDate", ""),
34
+ references=[row["notes"]] if row.get("notes") else [],
35
+ source="cisa_kev",
36
+ source_url=KEV_URL,
37
+ status="unverified",
38
+ ingested_at=now,
39
+ tags=["kev", "actively-exploited"],
40
+ )
41
+ if adv.cve_id:
42
+ advisories.append(adv)
43
+
44
+ return advisories