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.
- xpl_intel-1.2.0/LICENSE +123 -0
- xpl_intel-1.2.0/PKG-INFO +24 -0
- xpl_intel-1.2.0/README.md +55 -0
- xpl_intel-1.2.0/ingestion/__init__.py +2 -0
- xpl_intel-1.2.0/ingestion/cli.py +162 -0
- xpl_intel-1.2.0/ingestion/dedup.py +49 -0
- xpl_intel-1.2.0/ingestion/output/__init__.py +9 -0
- xpl_intel-1.2.0/ingestion/output/csv_writer.py +36 -0
- xpl_intel-1.2.0/ingestion/output/json_writer.py +17 -0
- xpl_intel-1.2.0/ingestion/output/sqlite_writer.py +116 -0
- xpl_intel-1.2.0/ingestion/sources/__init__.py +0 -0
- xpl_intel-1.2.0/ingestion/sources/cisa_kev.py +44 -0
- xpl_intel-1.2.0/ingestion/sources/epss_api.py +49 -0
- xpl_intel-1.2.0/ingestion/sources/exploitdb.py +50 -0
- xpl_intel-1.2.0/ingestion/sources/github_advisory.py +113 -0
- xpl_intel-1.2.0/ingestion/sources/nuclei_templates.py +62 -0
- xpl_intel-1.2.0/ingestion/sources/nvd_api.py +99 -0
- xpl_intel-1.2.0/ingestion/sources/packetstorm_rss.py +47 -0
- xpl_intel-1.2.0/ingestion/sources/vulncheck_kev.py +67 -0
- xpl_intel-1.2.0/ingestion/sources/watchtowr_rss.py +48 -0
- xpl_intel-1.2.0/pyproject.toml +83 -0
- xpl_intel-1.2.0/schemas/__init__.py +0 -0
- xpl_intel-1.2.0/schemas/advisory_v1.py +73 -0
- xpl_intel-1.2.0/schemas/py.typed +0 -0
- xpl_intel-1.2.0/setup.cfg +4 -0
- xpl_intel-1.2.0/tests/test_advisory.py +134 -0
- xpl_intel-1.2.0/tests/test_cli.py +110 -0
- xpl_intel-1.2.0/tests/test_core.py +103 -0
- xpl_intel-1.2.0/tests/test_dedup.py +115 -0
- xpl_intel-1.2.0/tests/test_epss.py +75 -0
- xpl_intel-1.2.0/tests/test_exploitdb.py +77 -0
- xpl_intel-1.2.0/tests/test_ghsa.py +156 -0
- xpl_intel-1.2.0/tests/test_kev.py +90 -0
- xpl_intel-1.2.0/tests/test_nuclei.py +123 -0
- xpl_intel-1.2.0/tests/test_nvd.py +197 -0
- xpl_intel-1.2.0/tests/test_output.py +186 -0
- xpl_intel-1.2.0/tests/test_packetstorm.py +68 -0
- xpl_intel-1.2.0/tests/test_vulncheck.py +105 -0
- xpl_intel-1.2.0/tests/test_watchtowr.py +101 -0
- xpl_intel-1.2.0/xpl_intel.egg-info/PKG-INFO +24 -0
- xpl_intel-1.2.0/xpl_intel.egg-info/SOURCES.txt +43 -0
- xpl_intel-1.2.0/xpl_intel.egg-info/dependency_links.txt +1 -0
- xpl_intel-1.2.0/xpl_intel.egg-info/entry_points.txt +3 -0
- xpl_intel-1.2.0/xpl_intel.egg-info/requires.txt +11 -0
- xpl_intel-1.2.0/xpl_intel.egg-info/top_level.txt +2 -0
xpl_intel-1.2.0/LICENSE
ADDED
|
@@ -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
|
xpl_intel-1.2.0/PKG-INFO
ADDED
|
@@ -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,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
|