oebc 0.1.0__py3-none-any.whl
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.
- oebc/__init__.py +6 -0
- oebc/__main__.py +4 -0
- oebc/cli.py +48 -0
- oebc/fetch.py +24 -0
- oebc/generate.py +305 -0
- oebc/urls.py +17 -0
- oebc-0.1.0.dist-info/METADATA +451 -0
- oebc-0.1.0.dist-info/RECORD +12 -0
- oebc-0.1.0.dist-info/WHEEL +5 -0
- oebc-0.1.0.dist-info/entry_points.txt +2 -0
- oebc-0.1.0.dist-info/licenses/LICENSE +21 -0
- oebc-0.1.0.dist-info/top_level.txt +1 -0
oebc/__init__.py
ADDED
oebc/__main__.py
ADDED
oebc/cli.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""CLI dispatcher for the oebc command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
|
|
7
|
+
from . import __version__
|
|
8
|
+
from .fetch import main as fetch_main
|
|
9
|
+
from .generate import _generate_parser
|
|
10
|
+
from .generate import main as generate_main
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main() -> None:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="oebc",
|
|
16
|
+
description="Open Exposure Baseline Catalog (OEBC) CLI",
|
|
17
|
+
)
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"--version",
|
|
20
|
+
action="version",
|
|
21
|
+
version=f"%(prog)s {__version__}",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
25
|
+
|
|
26
|
+
# fetch subcommand
|
|
27
|
+
fetch_p = subparsers.add_parser(
|
|
28
|
+
"fetch",
|
|
29
|
+
help="Download the pre-built catalog from openastra.org (fast)",
|
|
30
|
+
)
|
|
31
|
+
fetch_p.add_argument(
|
|
32
|
+
"--out",
|
|
33
|
+
default="oebc_full_catalog_enriched.json",
|
|
34
|
+
help="Output file path (default: oebc_full_catalog_enriched.json)",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# generate subcommand — inherits all flags from _generate_parser
|
|
38
|
+
subparsers.add_parser(
|
|
39
|
+
"generate",
|
|
40
|
+
help="Generate the catalog from scratch from NVD, EPSS, and CISA KEV sources",
|
|
41
|
+
parents=[_generate_parser(add_help=False)],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
args = parser.parse_args()
|
|
45
|
+
if args.command == "fetch":
|
|
46
|
+
fetch_main(args)
|
|
47
|
+
else:
|
|
48
|
+
generate_main(args)
|
oebc/fetch.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Download the pre-built OEBC catalog from openastra.org."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import gzip
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
import requests
|
|
10
|
+
|
|
11
|
+
from .urls import OEBC_CATALOG_URL
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main(args: argparse.Namespace) -> None:
|
|
15
|
+
print(f"Fetching catalog from {OEBC_CATALOG_URL} ...")
|
|
16
|
+
r = requests.get(OEBC_CATALOG_URL, timeout=60)
|
|
17
|
+
r.raise_for_status()
|
|
18
|
+
# Catalog is gzip-compressed. Decompress regardless of Content-Encoding header
|
|
19
|
+
# (GitHub Pages does not automatically set Content-Encoding for .gz files).
|
|
20
|
+
raw = gzip.decompress(r.content)
|
|
21
|
+
data = json.loads(raw)
|
|
22
|
+
with open(args.out, "w", encoding="utf-8") as f:
|
|
23
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
24
|
+
print(f"Wrote {data.get('count', '?')} vulnerabilities to {args.out}")
|
oebc/generate.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Core catalog generation logic for OEBC.
|
|
2
|
+
|
|
3
|
+
Fetches CVE data from NVD, EPSS scores from FIRST.org, and KEV list from CISA.
|
|
4
|
+
Joins all three by CVE ID, computes exposure_tier and actionable, writes JSON.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import requests
|
|
15
|
+
from requests.adapters import HTTPAdapter
|
|
16
|
+
from urllib3.util.retry import Retry
|
|
17
|
+
|
|
18
|
+
from .urls import EPSS_URL, KEV_URL, NVD_URL
|
|
19
|
+
|
|
20
|
+
# NVD API returns at most 2000 results per page
|
|
21
|
+
_NVD_PAGE_SIZE = 2000
|
|
22
|
+
|
|
23
|
+
# EPSS API page size — fetch in large batches
|
|
24
|
+
_EPSS_PAGE_SIZE = 10000
|
|
25
|
+
|
|
26
|
+
# Tier ordering for actionable comparison
|
|
27
|
+
_TIER_ORDER = {"LOW": 0, "MEDIUM": 1, "HIGH": 2, "CRITICAL": 3}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ─── Data fetching ───────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
def _make_session() -> requests.Session:
|
|
33
|
+
"""Return a requests Session with automatic retry on transient errors."""
|
|
34
|
+
session = requests.Session()
|
|
35
|
+
retry = Retry(
|
|
36
|
+
total=5,
|
|
37
|
+
backoff_factor=2,
|
|
38
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
39
|
+
allowed_methods=["GET"],
|
|
40
|
+
raise_on_status=False,
|
|
41
|
+
)
|
|
42
|
+
adapter = HTTPAdapter(max_retries=retry)
|
|
43
|
+
session.mount("https://", adapter)
|
|
44
|
+
session.mount("http://", adapter)
|
|
45
|
+
return session
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def fetch_nvd_cves(nvd_url: str, api_key: str | None = None) -> list[dict]:
|
|
49
|
+
"""Fetch all CVEs from the NVD API (paginated). Returns list of raw CVE dicts."""
|
|
50
|
+
cves: list[dict] = []
|
|
51
|
+
start = 0
|
|
52
|
+
total = None
|
|
53
|
+
|
|
54
|
+
while total is None or start < total:
|
|
55
|
+
params: dict = {"startIndex": start, "resultsPerPage": _NVD_PAGE_SIZE}
|
|
56
|
+
if api_key:
|
|
57
|
+
params["apiKey"] = api_key
|
|
58
|
+
r = _make_session().get(
|
|
59
|
+
nvd_url,
|
|
60
|
+
params=params,
|
|
61
|
+
timeout=60,
|
|
62
|
+
)
|
|
63
|
+
r.raise_for_status()
|
|
64
|
+
data = r.json()
|
|
65
|
+
total = data["totalResults"]
|
|
66
|
+
page_cves = [v["cve"] for v in data.get("vulnerabilities", [])]
|
|
67
|
+
cves.extend(page_cves)
|
|
68
|
+
start += len(page_cves)
|
|
69
|
+
if len(page_cves) == 0:
|
|
70
|
+
break
|
|
71
|
+
|
|
72
|
+
return cves
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def fetch_epss_scores(epss_url: str) -> dict[str, dict]:
|
|
76
|
+
"""Fetch all EPSS scores (paginated). Returns dict keyed by CVE ID."""
|
|
77
|
+
scores: dict[str, dict] = {}
|
|
78
|
+
offset = 0
|
|
79
|
+
total = None
|
|
80
|
+
|
|
81
|
+
while total is None or offset < total:
|
|
82
|
+
r = _make_session().get(
|
|
83
|
+
epss_url,
|
|
84
|
+
params={"offset": offset, "limit": _EPSS_PAGE_SIZE},
|
|
85
|
+
timeout=60,
|
|
86
|
+
)
|
|
87
|
+
r.raise_for_status()
|
|
88
|
+
data = r.json()
|
|
89
|
+
total = data["total"]
|
|
90
|
+
page_data = data.get("data", [])
|
|
91
|
+
for record in page_data:
|
|
92
|
+
scores[record["cve"]] = {
|
|
93
|
+
"epss": float(record["epss"]),
|
|
94
|
+
"percentile": float(record["percentile"]),
|
|
95
|
+
}
|
|
96
|
+
offset += len(page_data)
|
|
97
|
+
if len(page_data) == 0:
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
return scores
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def fetch_kev(kev_url: str) -> dict[str, dict]:
|
|
104
|
+
"""Fetch CISA KEV list. Returns dict keyed by CVE ID."""
|
|
105
|
+
r = _make_session().get(kev_url, timeout=30)
|
|
106
|
+
r.raise_for_status()
|
|
107
|
+
data = r.json()
|
|
108
|
+
return {
|
|
109
|
+
v["cveID"]: {
|
|
110
|
+
"date_added": v.get("dateAdded"),
|
|
111
|
+
"due_date": v.get("dueDate"),
|
|
112
|
+
"ransomware_use": v.get("knownRansomwareCampaignUse"),
|
|
113
|
+
}
|
|
114
|
+
for v in data.get("vulnerabilities", [])
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# ─── CVE field extraction ────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def extract_cvss(cve: dict) -> tuple[float | None, str | None, str | None, str | None]:
|
|
121
|
+
"""Extract (score, vector, version, severity) from a NVD CVE dict.
|
|
122
|
+
|
|
123
|
+
Prefers CVSS v3.1, then v3.0. Returns (None, None, None, None) if no score assigned.
|
|
124
|
+
"""
|
|
125
|
+
metrics = cve.get("metrics", {})
|
|
126
|
+
for key in ("cvssMetricV31", "cvssMetricV30"):
|
|
127
|
+
entries = metrics.get(key, [])
|
|
128
|
+
if entries:
|
|
129
|
+
d = entries[0]["cvssData"]
|
|
130
|
+
return (
|
|
131
|
+
d.get("baseScore"),
|
|
132
|
+
d.get("vectorString"),
|
|
133
|
+
d.get("version"),
|
|
134
|
+
d.get("baseSeverity"),
|
|
135
|
+
)
|
|
136
|
+
return None, None, None, None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def extract_cwes(cve: dict) -> list[str]:
|
|
140
|
+
"""Extract CWE IDs from a NVD CVE dict, excluding NVD placeholder values."""
|
|
141
|
+
_PLACEHOLDERS = {"NVD-CWE-noinfo", "NVD-CWE-Other"}
|
|
142
|
+
result = []
|
|
143
|
+
for weakness in cve.get("weaknesses", []):
|
|
144
|
+
for desc in weakness.get("description", []):
|
|
145
|
+
val = desc.get("value", "")
|
|
146
|
+
if desc.get("lang") == "en" and val not in _PLACEHOLDERS:
|
|
147
|
+
result.append(val)
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def extract_description(cve: dict) -> str:
|
|
152
|
+
"""Return the English description for a NVD CVE, or empty string if none."""
|
|
153
|
+
for desc in cve.get("descriptions", []):
|
|
154
|
+
if desc.get("lang") == "en":
|
|
155
|
+
return desc.get("value", "")
|
|
156
|
+
return ""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ─── Enrichment ──────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
def compute_exposure_tier(
|
|
162
|
+
kev_listed: bool,
|
|
163
|
+
epss_score: float | None,
|
|
164
|
+
cvss_score: float | None,
|
|
165
|
+
epss_high_threshold: float,
|
|
166
|
+
epss_medium_threshold: float,
|
|
167
|
+
) -> str:
|
|
168
|
+
"""Compute exposure tier. Rules evaluated in priority order; first match wins.
|
|
169
|
+
|
|
170
|
+
Tier ordering: LOW < MEDIUM < HIGH < CRITICAL
|
|
171
|
+
Null scores do not qualify any threshold comparison.
|
|
172
|
+
"""
|
|
173
|
+
if kev_listed:
|
|
174
|
+
return "CRITICAL"
|
|
175
|
+
epss_ok = epss_score is not None
|
|
176
|
+
cvss_ok = cvss_score is not None
|
|
177
|
+
if (epss_ok and epss_score >= epss_high_threshold) or (cvss_ok and cvss_score >= 9.0):
|
|
178
|
+
return "HIGH"
|
|
179
|
+
if (epss_ok and epss_score >= epss_medium_threshold) or (cvss_ok and cvss_score >= 7.0):
|
|
180
|
+
return "MEDIUM"
|
|
181
|
+
return "LOW"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def compute_actionable(exposure_tier: str, actionable_min_tier: str) -> bool:
|
|
185
|
+
"""Return True if exposure_tier >= actionable_min_tier (LOW < MEDIUM < HIGH < CRITICAL)."""
|
|
186
|
+
return _TIER_ORDER[exposure_tier] >= _TIER_ORDER[actionable_min_tier.upper()]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# ─── Catalog assembly ────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
def build_catalog(
|
|
192
|
+
nvd_cves: list[dict],
|
|
193
|
+
epss_scores: dict[str, dict],
|
|
194
|
+
kev_data: dict[str, dict],
|
|
195
|
+
args: argparse.Namespace,
|
|
196
|
+
) -> dict[str, Any]:
|
|
197
|
+
"""Join NVD + EPSS + KEV, compute derived fields, return catalog dict."""
|
|
198
|
+
vulnerabilities = []
|
|
199
|
+
for cve in nvd_cves:
|
|
200
|
+
cve_id = cve["id"]
|
|
201
|
+
cvss_score, cvss_vector, cvss_version, cvss_severity = extract_cvss(cve)
|
|
202
|
+
epss = epss_scores.get(cve_id)
|
|
203
|
+
epss_score = epss["epss"] if epss else None
|
|
204
|
+
epss_percentile = epss["percentile"] if epss else None
|
|
205
|
+
kev = kev_data.get(cve_id)
|
|
206
|
+
kev_listed = kev is not None
|
|
207
|
+
exposure_tier = compute_exposure_tier(
|
|
208
|
+
kev_listed, epss_score, cvss_score,
|
|
209
|
+
args.epss_high_threshold, args.epss_medium_threshold,
|
|
210
|
+
)
|
|
211
|
+
vulnerabilities.append({
|
|
212
|
+
"cve_id": cve_id,
|
|
213
|
+
"description": extract_description(cve),
|
|
214
|
+
"published_date": cve.get("published", "")[:10],
|
|
215
|
+
"last_modified_date": cve.get("lastModified", "")[:10],
|
|
216
|
+
"cwe_ids": extract_cwes(cve),
|
|
217
|
+
"cvss_score": cvss_score,
|
|
218
|
+
"cvss_vector": cvss_vector,
|
|
219
|
+
"cvss_version": cvss_version,
|
|
220
|
+
"cvss_severity": cvss_severity,
|
|
221
|
+
"epss_score": epss_score,
|
|
222
|
+
"epss_percentile": epss_percentile,
|
|
223
|
+
"kev_listed": kev_listed,
|
|
224
|
+
"kev_date_added": kev["date_added"] if kev else None,
|
|
225
|
+
"kev_due_date": kev["due_date"] if kev else None,
|
|
226
|
+
"kev_ransomware_use": kev["ransomware_use"] if kev else None,
|
|
227
|
+
"exposure_tier": exposure_tier,
|
|
228
|
+
"actionable": compute_actionable(exposure_tier, args.actionable_min_tier),
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
"project": "Open Exposure Baseline Catalog (OEBC)",
|
|
233
|
+
"project_version": _package_version(),
|
|
234
|
+
"generated_at_utc": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
235
|
+
"sources": {
|
|
236
|
+
"nvd": args.nvd_url,
|
|
237
|
+
"epss": args.epss_url,
|
|
238
|
+
"kev": args.kev_url,
|
|
239
|
+
},
|
|
240
|
+
"rules": {
|
|
241
|
+
"exposure_tier_definition": {
|
|
242
|
+
"critical": "kev_listed = true",
|
|
243
|
+
"high": f"epss_score >= {args.epss_high_threshold} OR cvss_score >= 9.0",
|
|
244
|
+
"medium": f"epss_score >= {args.epss_medium_threshold} OR cvss_score >= 7.0",
|
|
245
|
+
"low": "everything else",
|
|
246
|
+
},
|
|
247
|
+
"tier_order": "LOW < MEDIUM < HIGH < CRITICAL",
|
|
248
|
+
"actionable_min_tier": args.actionable_min_tier,
|
|
249
|
+
},
|
|
250
|
+
"count": len(vulnerabilities),
|
|
251
|
+
"vulnerabilities": vulnerabilities,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _package_version() -> str:
|
|
256
|
+
from . import __version__
|
|
257
|
+
return __version__
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ─── CLI entry point ─────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
def _generate_parser(add_help: bool = True) -> argparse.ArgumentParser:
|
|
263
|
+
"""Return the ArgumentParser for the generate subcommand."""
|
|
264
|
+
p = argparse.ArgumentParser(
|
|
265
|
+
prog="oebc generate",
|
|
266
|
+
description="Generate the OEBC catalog from NVD, EPSS, and CISA KEV sources.",
|
|
267
|
+
add_help=add_help,
|
|
268
|
+
)
|
|
269
|
+
p.add_argument("--out", default="oebc_full_catalog_enriched.json",
|
|
270
|
+
help="Output file path (default: oebc_full_catalog_enriched.json)")
|
|
271
|
+
p.add_argument("--actionable_min_tier", default="medium",
|
|
272
|
+
choices=["low", "medium", "high", "critical"],
|
|
273
|
+
help="Minimum tier for actionable=true (default: medium)")
|
|
274
|
+
p.add_argument("--epss_high_threshold", type=float, default=0.10,
|
|
275
|
+
help="EPSS score threshold for HIGH tier (default: 0.10)")
|
|
276
|
+
p.add_argument("--epss_medium_threshold", type=float, default=0.01,
|
|
277
|
+
help="EPSS score threshold for MEDIUM tier (default: 0.01)")
|
|
278
|
+
p.add_argument("--nvd_url", default=NVD_URL, help="Override NVD source URL")
|
|
279
|
+
p.add_argument("--epss_url", default=EPSS_URL, help="Override EPSS source URL")
|
|
280
|
+
p.add_argument("--kev_url", default=KEV_URL, help="Override CISA KEV source URL")
|
|
281
|
+
p.add_argument("--nvd_api_key", default=None,
|
|
282
|
+
help="NVD API key for higher rate limits (optional)")
|
|
283
|
+
return p
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def main(args: argparse.Namespace) -> None:
|
|
287
|
+
"""Generate the catalog from live sources and write to args.out."""
|
|
288
|
+
print(f"Fetching CVEs from NVD: {args.nvd_url}")
|
|
289
|
+
nvd_cves = fetch_nvd_cves(args.nvd_url, api_key=getattr(args, "nvd_api_key", None))
|
|
290
|
+
print(f" → {len(nvd_cves)} CVEs fetched")
|
|
291
|
+
|
|
292
|
+
print(f"Fetching EPSS scores: {args.epss_url}")
|
|
293
|
+
epss_scores = fetch_epss_scores(args.epss_url)
|
|
294
|
+
print(f" → {len(epss_scores)} EPSS records fetched")
|
|
295
|
+
|
|
296
|
+
print(f"Fetching CISA KEV: {args.kev_url}")
|
|
297
|
+
kev_data = fetch_kev(args.kev_url)
|
|
298
|
+
print(f" → {len(kev_data)} KEV entries fetched")
|
|
299
|
+
|
|
300
|
+
print("Building catalog...")
|
|
301
|
+
catalog = build_catalog(nvd_cves, epss_scores, kev_data, args)
|
|
302
|
+
|
|
303
|
+
with open(args.out, "w", encoding="utf-8") as f:
|
|
304
|
+
json.dump(catalog, f, ensure_ascii=False, indent=2)
|
|
305
|
+
print(f"Wrote {catalog['count']} vulnerabilities to {args.out}")
|
oebc/urls.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Default source URLs for OEBC data sources and published catalog.
|
|
2
|
+
|
|
3
|
+
Override any URL via CLI flags or by setting OEBC_*_URL environment variables.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# NVD CVE 2.0 API — paginated, max 2000 results per page
|
|
7
|
+
NVD_URL = "https://services.nvd.nist.gov/rest/json/cves/2.0"
|
|
8
|
+
|
|
9
|
+
# FIRST.org EPSS API — exploitation probability scores for all CVEs
|
|
10
|
+
EPSS_URL = "https://api.first.org/data/v1/epss"
|
|
11
|
+
|
|
12
|
+
# CISA Known Exploited Vulnerabilities catalog
|
|
13
|
+
KEV_URL = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
|
|
14
|
+
|
|
15
|
+
# Pre-built OEBC catalog published daily to openastra.org (gzip-compressed JSON)
|
|
16
|
+
# Update this constant when the schema major/minor version changes (e.g., v0.1 → v0.2)
|
|
17
|
+
OEBC_CATALOG_URL = "https://openastra.org/oebc/catalog/v0.1/latest.json.gz"
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oebc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Open Exposure Baseline Catalog — daily CVE exposure baseline enriched with EPSS, KEV, and derived exposure tiers.
|
|
5
|
+
Author: Karthikeyan Sadayamuthu
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://openastra.org
|
|
8
|
+
Project-URL: Source, https://github.com/sadayamuthu/oebc
|
|
9
|
+
Project-URL: Issues, https://github.com/sadayamuthu/oebc/issues
|
|
10
|
+
Project-URL: Catalog, https://openastra.org/oebc/catalog/v0.1/latest.json.gz
|
|
11
|
+
Keywords: cve,epss,kev,vulnerability,exposure,security,nvd
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Information Technology
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: requests>=2.31
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=6.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
31
|
+
Requires-Dist: pre-commit>=4.0; extra == "dev"
|
|
32
|
+
Requires-Dist: jsonschema>=4.0; extra == "dev"
|
|
33
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
<div align="center">
|
|
37
|
+
|
|
38
|
+
# Open Exposure Baseline Catalog (OEBC)
|
|
39
|
+
|
|
40
|
+
**Machine-Readable CVE Exposure Baseline enriched with EPSS, KEV, and Derived Exposure Tiers**
|
|
41
|
+
|
|
42
|
+
[](https://github.com/sadayamuthu/oebc/actions/workflows/develop.yml)
|
|
43
|
+
[](https://www.python.org/)
|
|
44
|
+
[](LICENSE)
|
|
45
|
+
[](https://openastra.org/oebc/catalog/v0.1/latest.json.gz)
|
|
46
|
+
[](https://openastra.org/oebc/schema/v0.1/oebc.json)
|
|
47
|
+
|
|
48
|
+
[Catalog](#catalog) · [Quick Start](#quick-start) · [CLI](#cli-usage) · [Schema](#output-schema) · [Automation](#automation)
|
|
49
|
+
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## What is OEBC?
|
|
55
|
+
|
|
56
|
+
OEBC is a daily, machine-readable, cloud-agnostic **exposure baseline catalog** derived from three authoritative sources:
|
|
57
|
+
|
|
58
|
+
- **[NVD](https://nvd.nist.gov/)** — CVE descriptions, CVSS scores, CWE IDs, published/modified dates
|
|
59
|
+
- **[FIRST.org EPSS](https://www.first.org/epss/)** — Exploitation probability score and percentile per CVE
|
|
60
|
+
- **[CISA KEV](https://www.cisa.gov/known-exploited-vulnerabilities-catalog)** — Known Exploited Vulnerabilities with ransomware use flags
|
|
61
|
+
|
|
62
|
+
Every CVE is enriched with derived fields (`exposure_tier`, `actionable`) and published as a gzip-compressed JSON catalog at:
|
|
63
|
+
|
|
64
|
+
**`https://openastra.org/oebc/catalog/v0.1/latest.json.gz`**
|
|
65
|
+
|
|
66
|
+
OEBC is consumed by **ExposureGate**, a CTEM (Continuous Threat Exposure Management) platform built on exposure baselines.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## How It Works
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
NVD CVE API FIRST.org EPSS CISA KEV
|
|
74
|
+
(250k+ CVEs) (EPSS scores) (Known exploited)
|
|
75
|
+
│ │ │
|
|
76
|
+
└────────────────────┴────────────────────┘
|
|
77
|
+
│
|
|
78
|
+
oebc generate
|
|
79
|
+
│
|
|
80
|
+
Join by CVE ID
|
|
81
|
+
│
|
|
82
|
+
Enrich: exposure_tier + actionable
|
|
83
|
+
│
|
|
84
|
+
┌────────────────┴──────────────────┐
|
|
85
|
+
│ │
|
|
86
|
+
latest.json.gz GitHub Release asset
|
|
87
|
+
(openastra.org repo) oebc-catalog-YYYY-MM-DD.json.gz
|
|
88
|
+
│
|
|
89
|
+
openastra.org/oebc/catalog/v0.1/
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Catalog
|
|
95
|
+
|
|
96
|
+
| Artifact | URL |
|
|
97
|
+
|----------|-----|
|
|
98
|
+
| Latest catalog (gzip) | `https://openastra.org/oebc/catalog/v0.1/latest.json.gz` |
|
|
99
|
+
| Historical snapshots | GitHub Release assets on each `baseline-YYYY-MM-DD` release |
|
|
100
|
+
| JSON Schema | `https://openastra.org/oebc/schema/v0.1/oebc.json` |
|
|
101
|
+
| YAML Schema | `https://openastra.org/oebc/schema/v0.1/oebc.yaml` |
|
|
102
|
+
|
|
103
|
+
> The catalog is gzip-compressed to stay under GitHub's 100 MB file size limit (raw JSON is ~290 MB; compressed is ~25–30 MB). Historical daily snapshots are attached as release assets rather than committed to the repo, keeping repo size stable.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Quick Start
|
|
108
|
+
|
|
109
|
+
**Download the pre-built catalog (fastest):**
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
# Download and decompress in one step
|
|
113
|
+
curl -L https://openastra.org/oebc/catalog/v0.1/latest.json.gz | gunzip > oebc_catalog.json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Or use the CLI:**
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install oebc
|
|
120
|
+
|
|
121
|
+
# Download pre-built catalog from openastra.org
|
|
122
|
+
oebc fetch
|
|
123
|
+
|
|
124
|
+
# Generate catalog from scratch (fetches all live sources)
|
|
125
|
+
oebc generate
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Or run from source:**
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
git clone https://github.com/sadayamuthu/oebc.git
|
|
132
|
+
cd oebc
|
|
133
|
+
make install-dev
|
|
134
|
+
make generate
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Features
|
|
140
|
+
|
|
141
|
+
- **250k+ CVEs** — full NVD catalog, updated daily
|
|
142
|
+
- **Three authoritative sources** — NVD, EPSS, CISA KEV joined by CVE ID
|
|
143
|
+
- **Derived exposure tiers** — CRITICAL / HIGH / MEDIUM / LOW from KEV, EPSS and CVSS
|
|
144
|
+
- **Actionable flag** — boolean shortcut for tiers at or above a configurable minimum
|
|
145
|
+
- **Null-safe enrichment** — missing CVSS or EPSS scores handled gracefully
|
|
146
|
+
- **Configurable thresholds** — EPSS cutoffs exposed as CLI flags (CVSS bands are fixed at 9.0 and 7.0)
|
|
147
|
+
- **Retry-resilient** — exponential backoff on NVD / EPSS / KEV HTTP failures
|
|
148
|
+
- **Versioned schema** — JSON Schema Draft 2020-12 with `additionalProperties: false`, checked by the test suite
|
|
149
|
+
- **PyPI package** — `pip install oebc`
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## CLI Usage
|
|
154
|
+
|
|
155
|
+
### `oebc fetch`
|
|
156
|
+
|
|
157
|
+
Download the pre-built catalog from openastra.org (fast, no live API calls).
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
oebc fetch
|
|
161
|
+
oebc fetch --out my-catalog.json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
| Flag | Default | Description |
|
|
165
|
+
|------|---------|-------------|
|
|
166
|
+
| `--out` | `oebc_full_catalog_enriched.json` | Output file path |
|
|
167
|
+
|
|
168
|
+
### `oebc generate`
|
|
169
|
+
|
|
170
|
+
Fetch NVD + EPSS + CISA KEV live and generate the full enriched catalog.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
oebc generate
|
|
174
|
+
oebc generate --out catalog.json \
|
|
175
|
+
--actionable_min_tier high \
|
|
176
|
+
--epss_high_threshold 0.10 \
|
|
177
|
+
--epss_medium_threshold 0.01 \
|
|
178
|
+
--nvd_api_key YOUR_KEY
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| Flag | Default | Description |
|
|
182
|
+
|------|---------|-------------|
|
|
183
|
+
| `--out` | `oebc_full_catalog_enriched.json` | Output file path |
|
|
184
|
+
| `--actionable_min_tier` | `medium` | Minimum tier for `actionable=true` (`low`, `medium`, `high`, `critical`) |
|
|
185
|
+
| `--epss_high_threshold` | `0.10` | EPSS score threshold for HIGH tier |
|
|
186
|
+
| `--epss_medium_threshold` | `0.01` | EPSS score threshold for MEDIUM tier |
|
|
187
|
+
| `--nvd_url` | NVD API URL | Override NVD source URL |
|
|
188
|
+
| `--epss_url` | FIRST.org EPSS URL | Override EPSS source URL |
|
|
189
|
+
| `--kev_url` | CISA KEV JSON URL | Override KEV source URL |
|
|
190
|
+
| `--nvd_api_key` | *(none)* | NVD API key for higher rate limits (optional) |
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## Output Schema
|
|
195
|
+
|
|
196
|
+
### Top-Level Structure
|
|
197
|
+
|
|
198
|
+
```json
|
|
199
|
+
{
|
|
200
|
+
"project": "Open Exposure Baseline Catalog (OEBC)",
|
|
201
|
+
"project_version": "0.1.0",
|
|
202
|
+
"generated_at_utc": "2026-03-16T06:00:00Z",
|
|
203
|
+
"sources": {
|
|
204
|
+
"nvd": "https://services.nvd.nist.gov/rest/json/cves/2.0",
|
|
205
|
+
"epss": "https://api.first.org/data/v1/epss",
|
|
206
|
+
"kev": "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
|
|
207
|
+
},
|
|
208
|
+
"rules": { ... },
|
|
209
|
+
"count": 250000,
|
|
210
|
+
"vulnerabilities": [ ... ]
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Per-CVE Record
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"cve_id": "CVE-2021-44228",
|
|
219
|
+
"description": "Apache Log4j2 JNDI...",
|
|
220
|
+
"published_date": "2021-12-10",
|
|
221
|
+
"last_modified_date": "2023-03-17",
|
|
222
|
+
"cwe_ids": ["CWE-917", "CWE-20"],
|
|
223
|
+
"cvss_score": 10.0,
|
|
224
|
+
"cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
|
|
225
|
+
"cvss_version": "3.1",
|
|
226
|
+
"cvss_severity": "CRITICAL",
|
|
227
|
+
"epss_score": 0.97566,
|
|
228
|
+
"epss_percentile": 0.99983,
|
|
229
|
+
"kev_listed": true,
|
|
230
|
+
"kev_date_added": "2021-12-10",
|
|
231
|
+
"kev_due_date": "2021-12-24",
|
|
232
|
+
"kev_ransomware_use": "Known",
|
|
233
|
+
"exposure_tier": "CRITICAL",
|
|
234
|
+
"actionable": true
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Field Reference
|
|
239
|
+
|
|
240
|
+
| Field | Type | Description |
|
|
241
|
+
|-------|------|-------------|
|
|
242
|
+
| `cve_id` | string | CVE identifier (e.g., `CVE-2021-44228`) |
|
|
243
|
+
| `description` | string | English description from NVD |
|
|
244
|
+
| `published_date` | string | ISO 8601 date first published |
|
|
245
|
+
| `last_modified_date` | string | ISO 8601 date last modified |
|
|
246
|
+
| `cwe_ids` | array | CWE identifiers (empty array if none) |
|
|
247
|
+
| `cvss_score` | number \| null | CVSS base score (prefers v3.1 > v3.0) |
|
|
248
|
+
| `cvss_vector` | string \| null | CVSS vector string |
|
|
249
|
+
| `cvss_version` | string \| null | CVSS version (`3.1`, `3.0`) |
|
|
250
|
+
| `cvss_severity` | string \| null | CVSS severity label |
|
|
251
|
+
| `epss_score` | number \| null | EPSS probability (0–1); null if not yet scored |
|
|
252
|
+
| `epss_percentile` | number \| null | EPSS percentile (0–1); null if not yet scored |
|
|
253
|
+
| `kev_listed` | boolean | `true` if in CISA KEV |
|
|
254
|
+
| `kev_date_added` | string \| null | Date added to KEV; null if not listed |
|
|
255
|
+
| `kev_due_date` | string \| null | CISA remediation due date; null if not listed |
|
|
256
|
+
| `kev_ransomware_use` | string \| null | Ransomware use flag from KEV; null if not listed |
|
|
257
|
+
| `exposure_tier` | string | Derived tier: `LOW`, `MEDIUM`, `HIGH`, `CRITICAL` |
|
|
258
|
+
| `actionable` | boolean | `true` if tier ≥ `actionable_min_tier` |
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## Exposure Tier Rules
|
|
263
|
+
|
|
264
|
+
Tiers are evaluated in priority order; first match wins.
|
|
265
|
+
|
|
266
|
+
| Tier | Condition |
|
|
267
|
+
|------|-----------|
|
|
268
|
+
| `CRITICAL` | `kev_listed = true` |
|
|
269
|
+
| `HIGH` | `epss_score >= 0.10` OR `cvss_score >= 9.0` |
|
|
270
|
+
| `MEDIUM` | `epss_score >= 0.01` OR `cvss_score >= 7.0` |
|
|
271
|
+
| `LOW` | everything else |
|
|
272
|
+
|
|
273
|
+
**Null handling:** null CVSS or EPSS scores evaluate as `false` in threshold comparisons — they never qualify for a higher tier.
|
|
274
|
+
|
|
275
|
+
**`actionable`:** `true` when `exposure_tier` ≥ `actionable_min_tier` (default: `medium`), so tiers `{MEDIUM, HIGH, CRITICAL}` are actionable by default.
|
|
276
|
+
|
|
277
|
+
**Threshold ordering:** `LOW < MEDIUM < HIGH < CRITICAL`
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Code Flow
|
|
282
|
+
|
|
283
|
+
```mermaid
|
|
284
|
+
flowchart TD
|
|
285
|
+
A[oebc generate] --> B[fetch_nvd_cves\nPaginated NVD API]
|
|
286
|
+
A --> C[fetch_epss_scores\nPaginated EPSS API]
|
|
287
|
+
A --> D[fetch_kev\nCISA KEV JSON]
|
|
288
|
+
B --> E[build_catalog\nJoin by CVE ID]
|
|
289
|
+
C --> E
|
|
290
|
+
D --> E
|
|
291
|
+
E --> F[compute_exposure_tier\nKEV → CRITICAL\nEPSS/CVSS → HIGH/MEDIUM\nelse → LOW]
|
|
292
|
+
F --> G[compute_actionable\ntier >= actionable_min_tier]
|
|
293
|
+
G --> H[Write JSON\nindent=2]
|
|
294
|
+
H --> I[oebc_full_catalog_enriched.json]
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Project Structure
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
oebc/
|
|
303
|
+
├── .github/
|
|
304
|
+
│ ├── dependabot.yml # Weekly grouped pip and Actions updates
|
|
305
|
+
│ └── workflows/
|
|
306
|
+
│ ├── main-release.yml # Daily catalog generation → publish to openastra.org
|
|
307
|
+
│ ├── schema-release.yml # Schema versioning → publish schema to openastra.org
|
|
308
|
+
│ ├── pypi-publish.yml # Publish to PyPI on v*.*.* tags
|
|
309
|
+
│ └── develop.yml # CI on PRs and pushes to develop branch
|
|
310
|
+
├── spec/
|
|
311
|
+
│ ├── VERSION # Semver schema version (e.g., 0.1.0)
|
|
312
|
+
│ └── schemas/
|
|
313
|
+
│ ├── oebc-v0.1.json # JSON Schema Draft 2020-12
|
|
314
|
+
│ └── oebc-v0.1.yaml # YAML equivalent
|
|
315
|
+
├── src/oebc/
|
|
316
|
+
│ ├── __init__.py # Package version
|
|
317
|
+
│ ├── __main__.py # Entry point
|
|
318
|
+
│ ├── cli.py # CLI dispatcher (fetch/generate)
|
|
319
|
+
│ ├── fetch.py # Download pre-built catalog from openastra.org
|
|
320
|
+
│ ├── generate.py # Core: fetch NVD+EPSS+KEV, join, enrich, output
|
|
321
|
+
│ └── urls.py # Default source URLs
|
|
322
|
+
├── tests/
|
|
323
|
+
│ ├── test_cli.py
|
|
324
|
+
│ ├── test_fetch.py
|
|
325
|
+
│ ├── test_generate.py
|
|
326
|
+
│ ├── test_schema_validation.py
|
|
327
|
+
│ └── test_version.py
|
|
328
|
+
├── CITATION.cff
|
|
329
|
+
├── CONTRIBUTING.md
|
|
330
|
+
├── LICENSE
|
|
331
|
+
├── Makefile
|
|
332
|
+
├── pyproject.toml
|
|
333
|
+
├── README.md
|
|
334
|
+
└── SECURITY.md
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
> **Note:** No `baseline/` or `historical/` directory. Catalog artifacts are generated ephemerally in CI and pushed directly to openastra.org — the repo stays clean.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Automation
|
|
342
|
+
|
|
343
|
+
OEBC ships four GitHub Actions workflows:
|
|
344
|
+
|
|
345
|
+
### `develop.yml` — Development CI
|
|
346
|
+
|
|
347
|
+
Triggered on push to `develop` and pull requests targeting `develop` or `main`.
|
|
348
|
+
|
|
349
|
+
- `make check`: lint with ruff, then the full test suite with 100% coverage enforced
|
|
350
|
+
- Runs on Python 3.11, 3.12, 3.13
|
|
351
|
+
|
|
352
|
+
### `main-release.yml` — Daily Catalog Generation
|
|
353
|
+
|
|
354
|
+
Triggered on schedule (daily 06:00 UTC), push to `main`, and manual dispatch.
|
|
355
|
+
|
|
356
|
+
1. Test across Python 3.11, 3.12, 3.13
|
|
357
|
+
2. `oebc generate --out oebc_full_catalog_enriched.json` (ephemeral)
|
|
358
|
+
3. gzip the catalog and push it as `public/oebc/catalog/v0.1/latest.json.gz` to the openastra.org site repository (served at `openastra.org/oebc/catalog/v0.1/latest.json.gz`)
|
|
359
|
+
4. If the catalog changed, create a GitHub Release tagged `baseline-YYYY-MM-DD` with the snapshot attached as `oebc-catalog-YYYY-MM-DD.json.gz`
|
|
360
|
+
|
|
361
|
+
Requires secret: `OEBC_DEPLOY_TOKEN`
|
|
362
|
+
|
|
363
|
+
### `schema-release.yml` — Schema Publishing
|
|
364
|
+
|
|
365
|
+
Triggered on push to `main` affecting `spec/**`.
|
|
366
|
+
|
|
367
|
+
1. Validate `spec/VERSION` (semver); abort if tag already exists
|
|
368
|
+
2. Publish schema files to `openastra.org/oebc/schema/v{MAJOR.MINOR}/`
|
|
369
|
+
3. Create GitHub Release tagged `spec-v{VERSION}` with schema files attached
|
|
370
|
+
|
|
371
|
+
Requires secret: `OEBC_DEPLOY_TOKEN`
|
|
372
|
+
|
|
373
|
+
### `pypi-publish.yml` — PyPI Publishing
|
|
374
|
+
|
|
375
|
+
Triggered on push of any `v*.*.*` tag.
|
|
376
|
+
|
|
377
|
+
1. Build Python package
|
|
378
|
+
2. Publish to PyPI via OIDC Trusted Publishing (no API token required)
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Development
|
|
383
|
+
|
|
384
|
+
```bash
|
|
385
|
+
# Install with dev dependencies + pre-commit hooks
|
|
386
|
+
make install-dev
|
|
387
|
+
|
|
388
|
+
# Run tests
|
|
389
|
+
make test
|
|
390
|
+
|
|
391
|
+
# Run tests with 100% coverage report
|
|
392
|
+
make test-cov
|
|
393
|
+
|
|
394
|
+
# Lint
|
|
395
|
+
make lint
|
|
396
|
+
|
|
397
|
+
# Auto-format
|
|
398
|
+
make format
|
|
399
|
+
|
|
400
|
+
# Lint + test-cov
|
|
401
|
+
make check
|
|
402
|
+
|
|
403
|
+
# Generate catalog locally
|
|
404
|
+
make generate
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
## Data Sources
|
|
410
|
+
|
|
411
|
+
| Source | URL | What it provides |
|
|
412
|
+
|--------|-----|-----------------|
|
|
413
|
+
| NVD CVE API | `https://services.nvd.nist.gov/rest/json/cves/2.0` | All CVEs: description, CVSS score/vector/version/severity, CWE IDs, dates |
|
|
414
|
+
| EPSS API | `https://api.first.org/data/v1/epss` | Exploitation probability score (0–1) + percentile per CVE |
|
|
415
|
+
| CISA KEV | `https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json` | Known exploited CVEs with `dateAdded`, `dueDate`, ransomware use |
|
|
416
|
+
|
|
417
|
+
All sources are fetched live at generation time. No local data files committed to the repo.
|
|
418
|
+
|
|
419
|
+
---
|
|
420
|
+
|
|
421
|
+
## Contributing
|
|
422
|
+
|
|
423
|
+
To report a wrong tier, a wrong EPSS or KEV join or a missing CVE, or to send
|
|
424
|
+
a change, see **[CONTRIBUTING.md](CONTRIBUTING.md)**. It covers the dev setup,
|
|
425
|
+
the 100% coverage rule, how tier rules change and how the schema is versioned.
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## Security
|
|
430
|
+
|
|
431
|
+
Please don't report security problems in a public issue. Use
|
|
432
|
+
[**Report a vulnerability**](https://github.com/sadayamuthu/oebc/security/advisories/new)
|
|
433
|
+
on the Security tab; see **[SECURITY.md](SECURITY.md)** for what's in scope.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
437
|
+
## License
|
|
438
|
+
|
|
439
|
+
[MIT License](LICENSE)
|
|
440
|
+
|
|
441
|
+
The license covers the code in this repository. The catalog republishes NVD,
|
|
442
|
+
FIRST EPSS and CISA KEV data, which stays under its providers' terms; see
|
|
443
|
+
[Upstream data](CONTRIBUTING.md#upstream-data).
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
<div align="center">
|
|
448
|
+
|
|
449
|
+
[Catalog](https://openastra.org/oebc/catalog/v0.1/latest.json.gz) · [Schema](https://openastra.org/oebc/schema/v0.1/oebc.json) · [PyPI](https://pypi.org/project/oebc/) · [openastra.org](https://openastra.org)
|
|
450
|
+
|
|
451
|
+
</div>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
oebc/__init__.py,sha256=cpF0Dixb94fOpfg8uXerg-k_pShDx-Kbdtsr5w2HRBo,176
|
|
2
|
+
oebc/__main__.py,sha256=jUDiCGRIhJvfwEslxaIxuuBq7RQwE_t_z0CFJRVfNl8,81
|
|
3
|
+
oebc/cli.py,sha256=BZ28pYNVRqg8uh9fCJRswt_opXpQ6nnqCQp1wIPAWpw,1312
|
|
4
|
+
oebc/fetch.py,sha256=I6juIlNEFKG2_RWc9S28nVdCPXGb8dE_6h8FUzpHVJ4,785
|
|
5
|
+
oebc/generate.py,sha256=811hq43RWjzc0Sc615J_eFvL8z3ZuRw9bgT2Nro7t0E,11664
|
|
6
|
+
oebc/urls.py,sha256=8Grp92uxWJ_rwdm2Ix1ClEFBfwBrSn87rkdKwDwJT14,783
|
|
7
|
+
oebc-0.1.0.dist-info/licenses/LICENSE,sha256=Rmq4mhzHm3g4XKkcf_IFAbiW9FPkQ2zeBx0yomCC7CY,1080
|
|
8
|
+
oebc-0.1.0.dist-info/METADATA,sha256=yh4g8qMcO9zldI8A5uCV9pbJupl0yfhRuCL79lY6D08,15813
|
|
9
|
+
oebc-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
10
|
+
oebc-0.1.0.dist-info/entry_points.txt,sha256=eCalFxww6KksZLKnWvDTC9ajXkwJyJ12pJnrxbeFAWY,39
|
|
11
|
+
oebc-0.1.0.dist-info/top_level.txt,sha256=8GXueB-P_e5IEnbqJTboNg3Tydzkr_VgVnruLzmH244,5
|
|
12
|
+
oebc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Karthikeyan Sadayamuthu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
oebc
|