icsprio 1.0.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.
- icsprio-1.0.0/.gitignore +20 -0
- icsprio-1.0.0/AUTHORS.json +27 -0
- icsprio-1.0.0/LICENSE +30 -0
- icsprio-1.0.0/LICENSE-DATA +20 -0
- icsprio-1.0.0/PKG-INFO +191 -0
- icsprio-1.0.0/README.md +146 -0
- icsprio-1.0.0/code/01_fetch.py +29 -0
- icsprio-1.0.0/code/02_build.py +31 -0
- icsprio-1.0.0/code/03_qa.py +36 -0
- icsprio-1.0.0/docs/ATTACK_MAPPING.md +60 -0
- icsprio-1.0.0/docs/CODEBOOK.md +44 -0
- icsprio-1.0.0/docs/LIMITATIONS.md +67 -0
- icsprio-1.0.0/docs/NEXT_STEPS.md +31 -0
- icsprio-1.0.0/docs/PUBLISH_GUIDE.md +134 -0
- icsprio-1.0.0/docs/SCORING.md +83 -0
- icsprio-1.0.0/docs/VERIFY_CHECKLIST.md +73 -0
- icsprio-1.0.0/icsprio/__init__.py +8 -0
- icsprio-1.0.0/icsprio/__main__.py +4 -0
- icsprio-1.0.0/icsprio/attack_mapping.py +57 -0
- icsprio-1.0.0/icsprio/cli.py +117 -0
- icsprio-1.0.0/icsprio/config.py +97 -0
- icsprio-1.0.0/icsprio/http.py +50 -0
- icsprio-1.0.0/icsprio/io_utils.py +39 -0
- icsprio-1.0.0/icsprio/join.py +123 -0
- icsprio-1.0.0/icsprio/pipeline.py +126 -0
- icsprio-1.0.0/icsprio/provenance.py +45 -0
- icsprio-1.0.0/icsprio/qa.py +142 -0
- icsprio-1.0.0/icsprio/reference_data/cwe_to_attack_ics.csv +46 -0
- icsprio-1.0.0/icsprio/scoring.py +105 -0
- icsprio-1.0.0/icsprio/sources/__init__.py +8 -0
- icsprio-1.0.0/icsprio/sources/attack_ics.py +85 -0
- icsprio-1.0.0/icsprio/sources/cisa_ics.py +208 -0
- icsprio-1.0.0/icsprio/sources/epss.py +85 -0
- icsprio-1.0.0/icsprio/sources/kev.py +73 -0
- icsprio-1.0.0/icsprio/sources/vulnrichment.py +152 -0
- icsprio-1.0.0/paper/paper.bib +24 -0
- icsprio-1.0.0/paper/paper.md +110 -0
- icsprio-1.0.0/pyproject.toml +82 -0
- icsprio-1.0.0/tests/conftest.py +36 -0
- icsprio-1.0.0/tests/fixtures/README.md +14 -0
- icsprio-1.0.0/tests/fixtures/attack_ics_sample.json +35 -0
- icsprio-1.0.0/tests/fixtures/csaf_advisory_sample.json +35 -0
- icsprio-1.0.0/tests/fixtures/epss_sample.json +14 -0
- icsprio-1.0.0/tests/fixtures/kev_sample.json +34 -0
- icsprio-1.0.0/tests/fixtures/vulnrichment_cve_sample.json +49 -0
- icsprio-1.0.0/tests/test_attack_ics.py +18 -0
- icsprio-1.0.0/tests/test_attack_mapping.py +32 -0
- icsprio-1.0.0/tests/test_cisa_ics.py +33 -0
- icsprio-1.0.0/tests/test_cli.py +46 -0
- icsprio-1.0.0/tests/test_epss.py +18 -0
- icsprio-1.0.0/tests/test_join.py +60 -0
- icsprio-1.0.0/tests/test_kev.py +14 -0
- icsprio-1.0.0/tests/test_pipeline.py +54 -0
- icsprio-1.0.0/tests/test_provenance.py +33 -0
- icsprio-1.0.0/tests/test_qa.py +48 -0
- icsprio-1.0.0/tests/test_scoring.py +59 -0
- icsprio-1.0.0/tests/test_vulnrichment.py +26 -0
icsprio-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*.egg-info/
|
|
4
|
+
.eggs/
|
|
5
|
+
build/
|
|
6
|
+
dist/
|
|
7
|
+
.pytest_cache/
|
|
8
|
+
.mypy_cache/
|
|
9
|
+
.ruff_cache/
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
.env
|
|
13
|
+
*.env
|
|
14
|
+
data/raw/*
|
|
15
|
+
!data/raw/.gitkeep
|
|
16
|
+
!data/raw/PROVENANCE.txt
|
|
17
|
+
.DS_Store
|
|
18
|
+
*.log
|
|
19
|
+
.coverage
|
|
20
|
+
htmlcov/
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Author block for icsprio. Every document, citation file, Zenodo record, manuscript author list, and commit identity reads from here.",
|
|
3
|
+
"authors": [
|
|
4
|
+
{
|
|
5
|
+
"name": "Friday Ogochukwu Ikwuogu",
|
|
6
|
+
"family": "Ikwuogu",
|
|
7
|
+
"given": "Friday Ogochukwu",
|
|
8
|
+
"orcid": "0009-0009-2222-1318",
|
|
9
|
+
"email": "Friday.ikwuogu@gmail.com",
|
|
10
|
+
"affiliation": "Independent Researcher, Odessa, Texas, USA",
|
|
11
|
+
"roles": ["Conceptualization", "Methodology", "Software", "Data curation", "Validation", "Writing - original draft"],
|
|
12
|
+
"corresponding": true
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"collaborators": [
|
|
16
|
+
{
|
|
17
|
+
"name": "David Mike-Ewewie",
|
|
18
|
+
"family": "Mike-Ewewie",
|
|
19
|
+
"given": "David",
|
|
20
|
+
"orcid": "",
|
|
21
|
+
"email": "mike_d63291@utpb.edu",
|
|
22
|
+
"affiliation": "Computer Science Department, University of Texas Permian Basin, Odessa, Texas, USA",
|
|
23
|
+
"roles": ["Validation", "Writing - review and editing"],
|
|
24
|
+
"corresponding": false
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
icsprio-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Friday Ogochukwu Ikwuogu
|
|
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.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
This license covers the icsprio SOURCE CODE only. The joined/derived data
|
|
26
|
+
files produced by running the pipeline (data/processed/*) are licensed
|
|
27
|
+
separately under CC BY 4.0 — see LICENSE-DATA — because they are derived from
|
|
28
|
+
third-party sources (CISA, FIRST.org, MITRE, the CVE Program) that require
|
|
29
|
+
attribution. Those sources' own terms of use also continue to apply; see
|
|
30
|
+
README.md § Sources.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Creative Commons Attribution 4.0 International (CC BY 4.0)
|
|
2
|
+
|
|
3
|
+
The joined/derived data files produced by icsprio (everything under
|
|
4
|
+
data/processed/, including icsprio_joined.csv and icsprio_joined.parquet)
|
|
5
|
+
are licensed under CC BY 4.0: https://creativecommons.org/licenses/by/4.0/
|
|
6
|
+
|
|
7
|
+
Attribution required when redistributing or building on these outputs:
|
|
8
|
+
|
|
9
|
+
Derived from icsprio (Ikwuogu & Mike-Ewewie), joining data from:
|
|
10
|
+
- CISA ICS Advisories (Cybersecurity and Infrastructure Security Agency,
|
|
11
|
+
U.S. Government work, public domain)
|
|
12
|
+
- CISA Known Exploited Vulnerabilities (KEV) Catalog (public domain)
|
|
13
|
+
- EPSS scores, FIRST.org, Inc. (see https://www.first.org/epss/ for terms)
|
|
14
|
+
- CISA Vulnrichment (https://github.com/cisagov/vulnrichment, CC0-1.0)
|
|
15
|
+
- MITRE ATT&CK for ICS (https://attack.mitre.org, subject to the ATT&CK
|
|
16
|
+
Terms of Use: https://attack.mitre.org/resources/terms-of-use/)
|
|
17
|
+
|
|
18
|
+
Each underlying source's own license/terms continue to apply to that
|
|
19
|
+
source's contribution to the joined output; CC BY 4.0 governs icsprio's own
|
|
20
|
+
joining, scoring, and derived columns.
|
icsprio-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: icsprio
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Fetch, join, and prioritize ICS vulnerability intelligence from CISA ICS Advisories, KEV, EPSS, Vulnrichment, and ATT&CK for ICS.
|
|
5
|
+
Project-URL: Homepage, https://github.com/foikwuogu/icsprio
|
|
6
|
+
Project-URL: Repository, https://github.com/foikwuogu/icsprio
|
|
7
|
+
Project-URL: Issues, https://github.com/foikwuogu/icsprio/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/foikwuogu/icsprio/tree/main/docs
|
|
9
|
+
Author-email: Friday Ogochukwu Ikwuogu <Friday.ikwuogu@gmail.com>
|
|
10
|
+
Maintainer-email: Friday Ogochukwu Ikwuogu <Friday.ikwuogu@gmail.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: AUTHORS.json
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
License-File: LICENSE-DATA
|
|
15
|
+
Keywords: attack,cisa,critical-infrastructure,cybersecurity,epss,ics,kev,ot,scada,vulnerability-management,vulnrichment
|
|
16
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
17
|
+
Classifier: Intended Audience :: Information Technology
|
|
18
|
+
Classifier: Intended Audience :: Science/Research
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Operating System :: OS Independent
|
|
21
|
+
Classifier: Programming Language :: Python :: 3
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
25
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
26
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
27
|
+
Classifier: Topic :: Security
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Requires-Dist: click>=8.1
|
|
30
|
+
Requires-Dist: pandas>=2.0
|
|
31
|
+
Requires-Dist: pyyaml>=6.0
|
|
32
|
+
Requires-Dist: requests>=2.31
|
|
33
|
+
Provides-Extra: dashboard
|
|
34
|
+
Requires-Dist: streamlit>=1.57; extra == 'dashboard'
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest-cov>=4.1; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
39
|
+
Requires-Dist: responses>=0.24; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
41
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
42
|
+
Provides-Extra: parquet
|
|
43
|
+
Requires-Dist: pyarrow>=14.0; extra == 'parquet'
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
|
|
46
|
+
# icsprio
|
|
47
|
+
|
|
48
|
+
[](https://doi.org/10.5281/zenodo.22732040)
|
|
49
|
+
|
|
50
|
+
**Status:** v1.0.0 (verified — see [Verification](#verification)) | **Maintainer:** Friday Ogochukwu Ikwuogu, [ORCID 0009-0009-2222-1318](https://orcid.org/0009-0009-2222-1318) | **License:** code [MIT](LICENSE), joined data [CC BY 4.0](LICENSE-DATA)
|
|
51
|
+
|
|
52
|
+
icsprio is an open Python package that fetches, joins, and prioritizes ICS
|
|
53
|
+
(industrial control systems) vulnerability intelligence from five public
|
|
54
|
+
sources — CISA ICS Advisories, the CISA Known Exploited Vulnerabilities
|
|
55
|
+
(KEV) catalog, FIRST.org's EPSS, CISA's Vulnrichment SSVC/CVSS enrichment,
|
|
56
|
+
and MITRE ATT&CK for ICS — into one deterministic, reproducible table with
|
|
57
|
+
an OT-relevant priority score. It is built for operators, researchers, and
|
|
58
|
+
auditors who need a defensible, explainable answer to "which of these ICS
|
|
59
|
+
advisories should we act on first," instead of five browser tabs and a
|
|
60
|
+
spreadsheet.
|
|
61
|
+
|
|
62
|
+
Every number icsprio produces is reproducible: given the same fetched data
|
|
63
|
+
and the same scoring weights, two runs never disagree, and every weight and
|
|
64
|
+
mapping decision is documented and author-reviewed, not a black box.
|
|
65
|
+
|
|
66
|
+
## What is here
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
icsprio/ the installable package (fetchers, join, scoring, QA, CLI)
|
|
70
|
+
icsprio/reference_data/ cwe_to_attack_ics.csv — the author-reviewed CWE->ATT&CK crosswalk
|
|
71
|
+
code/ numbered scripts in run order (thin wrappers over icsprio/)
|
|
72
|
+
data/raw/ inputs as fetched, with PROVENANCE.txt (created by `fetch`)
|
|
73
|
+
data/processed/ joined table, qa_report.txt, stats.json (created by `build`)
|
|
74
|
+
docs/ CODEBOOK, SCORING, ATTACK_MAPPING, LIMITATIONS, VERIFY_CHECKLIST, NEXT_STEPS
|
|
75
|
+
tests/ offline pytest suite (fixtures only, no network calls)
|
|
76
|
+
paper/ JOSS-format paper draft (prepared, not yet submitted)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Install
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install icsprio # once published to PyPI
|
|
83
|
+
# or, from a clone:
|
|
84
|
+
pip install -e ".[dev,parquet]"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Requires Python 3.9+. Runtime dependencies are deliberately minimal:
|
|
88
|
+
`requests`, `pandas`, `click`, `PyYAML`.
|
|
89
|
+
|
|
90
|
+
## Run it
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
icsprio run # fetch + join + score + QA in one call
|
|
94
|
+
icsprio report # print the QA report
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
or, to re-score already-fetched data (e.g. after editing `docs/SCORING.md`'s
|
|
98
|
+
weights) without hitting the network again:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
icsprio fetch --since-year 2023
|
|
102
|
+
icsprio build --weights-file my_weights.yaml
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
or the numbered scripts, if you'd rather not install the CLI:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
python code/01_fetch.py --since-year 2023
|
|
109
|
+
python code/02_build.py
|
|
110
|
+
python code/03_qa.py
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
A stranger should be able to run any of the above from this README alone
|
|
114
|
+
and get `data/processed/icsprio_joined.csv` and `qa_report.txt`.
|
|
115
|
+
|
|
116
|
+
### Default scope
|
|
117
|
+
|
|
118
|
+
By default, `fetch` pulls ICS advisories from the last 3 years
|
|
119
|
+
(`--since-year <current year - 3>`), not the full historical archive. This
|
|
120
|
+
is a deliberate default, not a limitation of the data — see
|
|
121
|
+
[docs/LIMITATIONS.md](docs/LIMITATIONS.md) for why, and pass
|
|
122
|
+
`--since-year 2010` (or any earlier year) for a full historical pull. Set
|
|
123
|
+
the `GITHUB_TOKEN` environment variable to raise GitHub's API rate limit
|
|
124
|
+
for larger pulls.
|
|
125
|
+
|
|
126
|
+
## Sources
|
|
127
|
+
|
|
128
|
+
| Source | What it provides | Vintage | License / terms | Accessed |
|
|
129
|
+
|---|---|---|---|---|
|
|
130
|
+
| [CISA ICS Advisories (CSAF 2.0)](https://www.cisa.gov/news-events/ics-advisories), via [cisagov/CSAF](https://github.com/cisagov/CSAF) | Advisory metadata, vendor/product, CVEs | ongoing | U.S. Government work, public domain | 2026-09-13 |
|
|
131
|
+
| [CISA KEV catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) | Confirmed real-world exploitation | updated continuously | U.S. Government work, public domain | 2026-09-13 |
|
|
132
|
+
| [FIRST.org EPSS](https://www.first.org/epss/) | Exploitation-likelihood score | daily, back to 2021-04-14 | FIRST.org terms of use | 2026-09-13 |
|
|
133
|
+
| [CISA Vulnrichment](https://github.com/cisagov/vulnrichment) | SSVC decision points, CVSS, CWE | ongoing (partial CVE coverage) | CC0-1.0 | 2026-09-13 |
|
|
134
|
+
| [MITRE ATT&CK for ICS](https://github.com/mitre-attack/attack-stix-data) | Adversary technique catalog | versioned releases | ATT&CK Terms of Use | 2026-09-13 |
|
|
135
|
+
|
|
136
|
+
## How prioritization works
|
|
137
|
+
|
|
138
|
+
`priority_score` (0-100) is a deterministic weighted combination of KEV
|
|
139
|
+
status, EPSS percentile, and Vulnrichment's SSVC decision points. The exact
|
|
140
|
+
weights and the rationale for each are in [docs/SCORING.md](docs/SCORING.md)
|
|
141
|
+
— **read that before trusting or citing a score**; it is icsprio's own
|
|
142
|
+
analytic contribution, not a value CISA or FIRST publishes. `attack_technique_ids`
|
|
143
|
+
comes from a documented, author-reviewed CWE-to-ATT&CK-for-ICS heuristic
|
|
144
|
+
crosswalk (no authoritative CVE-to-technique mapping exists) — see
|
|
145
|
+
[docs/ATTACK_MAPPING.md](docs/ATTACK_MAPPING.md).
|
|
146
|
+
|
|
147
|
+
## Headline numbers
|
|
148
|
+
|
|
149
|
+
From `data/processed/stats.json`, generated by a real `icsprio run` against
|
|
150
|
+
live sources (2026-09-13, `--since-year 2023`):
|
|
151
|
+
|
|
152
|
+
- 1,685 advisories fetched, 8,653 (advisory, CVE) rows, 7,107 unique CVEs
|
|
153
|
+
- Join match rates: KEV 1.0%, EPSS 99.7%, Vulnrichment 78.9%, ATT&CK-for-ICS mapped 5.9%
|
|
154
|
+
- Priority bands: Critical 83, High 37, Medium 1,978, Low 6,555
|
|
155
|
+
- 0 sanity-check problems
|
|
156
|
+
|
|
157
|
+
Re-run `icsprio run` yourself and these numbers regenerate from your own
|
|
158
|
+
live fetch — never take them as static.
|
|
159
|
+
|
|
160
|
+
## Verification
|
|
161
|
+
|
|
162
|
+
This release has cleared the author verification gate
|
|
163
|
+
([docs/VERIFY_CHECKLIST.md](docs/VERIFY_CHECKLIST.md)): a live `icsprio run`
|
|
164
|
+
against real sources, the full `pytest` suite passing, and a hand review of
|
|
165
|
+
`docs/SCORING.md`'s weights and `icsprio/reference_data/cwe_to_attack_ics.csv`'s
|
|
166
|
+
crosswalk.
|
|
167
|
+
|
|
168
|
+
See [docs/LIMITATIONS.md](docs/LIMITATIONS.md) for what remains provisional
|
|
169
|
+
by design (e.g. the crosswalk is a documented heuristic, not a MITRE
|
|
170
|
+
mapping) rather than unfinished.
|
|
171
|
+
|
|
172
|
+
## Publishing
|
|
173
|
+
|
|
174
|
+
Published on GitHub and archived on Zenodo
|
|
175
|
+
([10.5281/zenodo.22732040](https://doi.org/10.5281/zenodo.22732040)); see
|
|
176
|
+
[docs/PUBLISH_GUIDE.md](docs/PUBLISH_GUIDE.md) for PyPI steps.
|
|
177
|
+
|
|
178
|
+
## Citation
|
|
179
|
+
|
|
180
|
+
See [CITATION.cff](CITATION.cff). DOI:
|
|
181
|
+
[10.5281/zenodo.22732040](https://doi.org/10.5281/zenodo.22732040).
|
|
182
|
+
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
186
|
+
|
|
187
|
+
## Authors
|
|
188
|
+
|
|
189
|
+
See [AUTHORS.json](AUTHORS.json) — the single source of truth for
|
|
190
|
+
authorship across this README, CITATION.cff, the JOSS paper, and Zenodo
|
|
191
|
+
metadata.
|
icsprio-1.0.0/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# icsprio
|
|
2
|
+
|
|
3
|
+
[](https://doi.org/10.5281/zenodo.22732040)
|
|
4
|
+
|
|
5
|
+
**Status:** v1.0.0 (verified — see [Verification](#verification)) | **Maintainer:** Friday Ogochukwu Ikwuogu, [ORCID 0009-0009-2222-1318](https://orcid.org/0009-0009-2222-1318) | **License:** code [MIT](LICENSE), joined data [CC BY 4.0](LICENSE-DATA)
|
|
6
|
+
|
|
7
|
+
icsprio is an open Python package that fetches, joins, and prioritizes ICS
|
|
8
|
+
(industrial control systems) vulnerability intelligence from five public
|
|
9
|
+
sources — CISA ICS Advisories, the CISA Known Exploited Vulnerabilities
|
|
10
|
+
(KEV) catalog, FIRST.org's EPSS, CISA's Vulnrichment SSVC/CVSS enrichment,
|
|
11
|
+
and MITRE ATT&CK for ICS — into one deterministic, reproducible table with
|
|
12
|
+
an OT-relevant priority score. It is built for operators, researchers, and
|
|
13
|
+
auditors who need a defensible, explainable answer to "which of these ICS
|
|
14
|
+
advisories should we act on first," instead of five browser tabs and a
|
|
15
|
+
spreadsheet.
|
|
16
|
+
|
|
17
|
+
Every number icsprio produces is reproducible: given the same fetched data
|
|
18
|
+
and the same scoring weights, two runs never disagree, and every weight and
|
|
19
|
+
mapping decision is documented and author-reviewed, not a black box.
|
|
20
|
+
|
|
21
|
+
## What is here
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
icsprio/ the installable package (fetchers, join, scoring, QA, CLI)
|
|
25
|
+
icsprio/reference_data/ cwe_to_attack_ics.csv — the author-reviewed CWE->ATT&CK crosswalk
|
|
26
|
+
code/ numbered scripts in run order (thin wrappers over icsprio/)
|
|
27
|
+
data/raw/ inputs as fetched, with PROVENANCE.txt (created by `fetch`)
|
|
28
|
+
data/processed/ joined table, qa_report.txt, stats.json (created by `build`)
|
|
29
|
+
docs/ CODEBOOK, SCORING, ATTACK_MAPPING, LIMITATIONS, VERIFY_CHECKLIST, NEXT_STEPS
|
|
30
|
+
tests/ offline pytest suite (fixtures only, no network calls)
|
|
31
|
+
paper/ JOSS-format paper draft (prepared, not yet submitted)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install icsprio # once published to PyPI
|
|
38
|
+
# or, from a clone:
|
|
39
|
+
pip install -e ".[dev,parquet]"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Requires Python 3.9+. Runtime dependencies are deliberately minimal:
|
|
43
|
+
`requests`, `pandas`, `click`, `PyYAML`.
|
|
44
|
+
|
|
45
|
+
## Run it
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
icsprio run # fetch + join + score + QA in one call
|
|
49
|
+
icsprio report # print the QA report
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
or, to re-score already-fetched data (e.g. after editing `docs/SCORING.md`'s
|
|
53
|
+
weights) without hitting the network again:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
icsprio fetch --since-year 2023
|
|
57
|
+
icsprio build --weights-file my_weights.yaml
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
or the numbered scripts, if you'd rather not install the CLI:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python code/01_fetch.py --since-year 2023
|
|
64
|
+
python code/02_build.py
|
|
65
|
+
python code/03_qa.py
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
A stranger should be able to run any of the above from this README alone
|
|
69
|
+
and get `data/processed/icsprio_joined.csv` and `qa_report.txt`.
|
|
70
|
+
|
|
71
|
+
### Default scope
|
|
72
|
+
|
|
73
|
+
By default, `fetch` pulls ICS advisories from the last 3 years
|
|
74
|
+
(`--since-year <current year - 3>`), not the full historical archive. This
|
|
75
|
+
is a deliberate default, not a limitation of the data — see
|
|
76
|
+
[docs/LIMITATIONS.md](docs/LIMITATIONS.md) for why, and pass
|
|
77
|
+
`--since-year 2010` (or any earlier year) for a full historical pull. Set
|
|
78
|
+
the `GITHUB_TOKEN` environment variable to raise GitHub's API rate limit
|
|
79
|
+
for larger pulls.
|
|
80
|
+
|
|
81
|
+
## Sources
|
|
82
|
+
|
|
83
|
+
| Source | What it provides | Vintage | License / terms | Accessed |
|
|
84
|
+
|---|---|---|---|---|
|
|
85
|
+
| [CISA ICS Advisories (CSAF 2.0)](https://www.cisa.gov/news-events/ics-advisories), via [cisagov/CSAF](https://github.com/cisagov/CSAF) | Advisory metadata, vendor/product, CVEs | ongoing | U.S. Government work, public domain | 2026-09-13 |
|
|
86
|
+
| [CISA KEV catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) | Confirmed real-world exploitation | updated continuously | U.S. Government work, public domain | 2026-09-13 |
|
|
87
|
+
| [FIRST.org EPSS](https://www.first.org/epss/) | Exploitation-likelihood score | daily, back to 2021-04-14 | FIRST.org terms of use | 2026-09-13 |
|
|
88
|
+
| [CISA Vulnrichment](https://github.com/cisagov/vulnrichment) | SSVC decision points, CVSS, CWE | ongoing (partial CVE coverage) | CC0-1.0 | 2026-09-13 |
|
|
89
|
+
| [MITRE ATT&CK for ICS](https://github.com/mitre-attack/attack-stix-data) | Adversary technique catalog | versioned releases | ATT&CK Terms of Use | 2026-09-13 |
|
|
90
|
+
|
|
91
|
+
## How prioritization works
|
|
92
|
+
|
|
93
|
+
`priority_score` (0-100) is a deterministic weighted combination of KEV
|
|
94
|
+
status, EPSS percentile, and Vulnrichment's SSVC decision points. The exact
|
|
95
|
+
weights and the rationale for each are in [docs/SCORING.md](docs/SCORING.md)
|
|
96
|
+
— **read that before trusting or citing a score**; it is icsprio's own
|
|
97
|
+
analytic contribution, not a value CISA or FIRST publishes. `attack_technique_ids`
|
|
98
|
+
comes from a documented, author-reviewed CWE-to-ATT&CK-for-ICS heuristic
|
|
99
|
+
crosswalk (no authoritative CVE-to-technique mapping exists) — see
|
|
100
|
+
[docs/ATTACK_MAPPING.md](docs/ATTACK_MAPPING.md).
|
|
101
|
+
|
|
102
|
+
## Headline numbers
|
|
103
|
+
|
|
104
|
+
From `data/processed/stats.json`, generated by a real `icsprio run` against
|
|
105
|
+
live sources (2026-09-13, `--since-year 2023`):
|
|
106
|
+
|
|
107
|
+
- 1,685 advisories fetched, 8,653 (advisory, CVE) rows, 7,107 unique CVEs
|
|
108
|
+
- Join match rates: KEV 1.0%, EPSS 99.7%, Vulnrichment 78.9%, ATT&CK-for-ICS mapped 5.9%
|
|
109
|
+
- Priority bands: Critical 83, High 37, Medium 1,978, Low 6,555
|
|
110
|
+
- 0 sanity-check problems
|
|
111
|
+
|
|
112
|
+
Re-run `icsprio run` yourself and these numbers regenerate from your own
|
|
113
|
+
live fetch — never take them as static.
|
|
114
|
+
|
|
115
|
+
## Verification
|
|
116
|
+
|
|
117
|
+
This release has cleared the author verification gate
|
|
118
|
+
([docs/VERIFY_CHECKLIST.md](docs/VERIFY_CHECKLIST.md)): a live `icsprio run`
|
|
119
|
+
against real sources, the full `pytest` suite passing, and a hand review of
|
|
120
|
+
`docs/SCORING.md`'s weights and `icsprio/reference_data/cwe_to_attack_ics.csv`'s
|
|
121
|
+
crosswalk.
|
|
122
|
+
|
|
123
|
+
See [docs/LIMITATIONS.md](docs/LIMITATIONS.md) for what remains provisional
|
|
124
|
+
by design (e.g. the crosswalk is a documented heuristic, not a MITRE
|
|
125
|
+
mapping) rather than unfinished.
|
|
126
|
+
|
|
127
|
+
## Publishing
|
|
128
|
+
|
|
129
|
+
Published on GitHub and archived on Zenodo
|
|
130
|
+
([10.5281/zenodo.22732040](https://doi.org/10.5281/zenodo.22732040)); see
|
|
131
|
+
[docs/PUBLISH_GUIDE.md](docs/PUBLISH_GUIDE.md) for PyPI steps.
|
|
132
|
+
|
|
133
|
+
## Citation
|
|
134
|
+
|
|
135
|
+
See [CITATION.cff](CITATION.cff). DOI:
|
|
136
|
+
[10.5281/zenodo.22732040](https://doi.org/10.5281/zenodo.22732040).
|
|
137
|
+
|
|
138
|
+
## Contributing
|
|
139
|
+
|
|
140
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
141
|
+
|
|
142
|
+
## Authors
|
|
143
|
+
|
|
144
|
+
See [AUTHORS.json](AUTHORS.json) — the single source of truth for
|
|
145
|
+
authorship across this README, CITATION.cff, the JOSS paper, and Zenodo
|
|
146
|
+
metadata.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fetch every source and write data/raw/_fetch_manifest.json.
|
|
3
|
+
|
|
4
|
+
Equivalent to `icsprio fetch`; kept as a numbered script so the repository
|
|
5
|
+
matches the project's standard run-in-order layout even for someone who
|
|
6
|
+
hasn't installed the package as a CLI.
|
|
7
|
+
|
|
8
|
+
Usage: python code/01_fetch.py [--since-year YYYY]
|
|
9
|
+
"""
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
15
|
+
|
|
16
|
+
from icsprio import pipeline # noqa: E402
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
ap = argparse.ArgumentParser()
|
|
21
|
+
ap.add_argument("--since-year", type=int, default=None)
|
|
22
|
+
args = ap.parse_args()
|
|
23
|
+
manifest = pipeline.run_fetch(since_year=args.since_year)
|
|
24
|
+
for stage, count in manifest["stage_counts"].items():
|
|
25
|
+
print(f"{stage}: {count}")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Join, score, and write data/processed/icsprio_joined.{csv,parquet}.
|
|
3
|
+
|
|
4
|
+
Usage: python code/02_build.py [--weights-file weights.yaml]
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
11
|
+
|
|
12
|
+
from icsprio import io_utils, pipeline # noqa: E402
|
|
13
|
+
from icsprio.cli import _load_weights # noqa: E402
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
ap = argparse.ArgumentParser()
|
|
18
|
+
ap.add_argument("--weights-file", default=None)
|
|
19
|
+
args = ap.parse_args()
|
|
20
|
+
|
|
21
|
+
manifest = pipeline.load_manifest()
|
|
22
|
+
weights = _load_weights(args.weights_file)
|
|
23
|
+
df = pipeline.run_build(manifest, weights=weights)
|
|
24
|
+
|
|
25
|
+
io_utils.export_csv(df, "data/processed/icsprio_joined.csv")
|
|
26
|
+
io_utils.export_parquet(df, "data/processed/icsprio_joined.parquet")
|
|
27
|
+
print(f"Wrote {len(df)} rows to data/processed/icsprio_joined.csv")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
main()
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate data/processed/qa_report.txt and stats.json from the built table.
|
|
3
|
+
|
|
4
|
+
Usage: python code/03_qa.py [--final]
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
11
|
+
|
|
12
|
+
import pandas as pd # noqa: E402
|
|
13
|
+
|
|
14
|
+
from icsprio import io_utils, pipeline # noqa: E402
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main():
|
|
18
|
+
ap = argparse.ArgumentParser()
|
|
19
|
+
ap.add_argument("--final", action="store_true")
|
|
20
|
+
args = ap.parse_args()
|
|
21
|
+
|
|
22
|
+
manifest = pipeline.load_manifest()
|
|
23
|
+
df = pd.read_csv("data/processed/icsprio_joined.csv")
|
|
24
|
+
# list-typed columns round-trip through CSV as "a; b" strings; restore lists
|
|
25
|
+
for col in ("vendors", "products", "cwes", "attack_technique_ids", "attack_technique_names"):
|
|
26
|
+
df[col] = df[col].fillna("").apply(lambda v: [s for s in str(v).split("; ") if s])
|
|
27
|
+
|
|
28
|
+
stats, report_text = pipeline.run_qa(df, manifest["stage_counts"], draft=not args.final)
|
|
29
|
+
io_utils.write_stats_json(stats, "data/processed/stats.json")
|
|
30
|
+
with open("data/processed/qa_report.txt", "w", encoding="utf-8") as f:
|
|
31
|
+
f.write(report_text)
|
|
32
|
+
print(report_text)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if __name__ == "__main__":
|
|
36
|
+
main()
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# CWE -> ATT&CK-for-ICS crosswalk methodology
|
|
2
|
+
|
|
3
|
+
**Author-reviewed heuristic:** This crosswalk is icsprio's own heuristic,
|
|
4
|
+
authored for this project. It is not a MITRE, CISA, or CVE Program mapping;
|
|
5
|
+
every row in `icsprio/reference_data/cwe_to_attack_ics.csv` has been reviewed
|
|
6
|
+
by the author.
|
|
7
|
+
|
|
8
|
+
## Why a crosswalk is needed at all
|
|
9
|
+
|
|
10
|
+
No authoritative mapping from a CVE (or its CWE weakness class) to ATT&CK
|
|
11
|
+
for ICS techniques exists. ATT&CK for ICS catalogs *adversary behavior after
|
|
12
|
+
an initial foothold or during an intrusion* (e.g. "Modify Parameter," "Denial
|
|
13
|
+
of Service"); CWE catalogs *root-cause weakness types* in software (e.g.
|
|
14
|
+
"Out-of-bounds Write," "Missing Authentication"). They describe different
|
|
15
|
+
things at different points in an intrusion, so there is no clean 1:1
|
|
16
|
+
mapping — this crosswalk is a documented, reviewable judgment call about
|
|
17
|
+
which techniques an adversary would plausibly use *given* a vulnerability of
|
|
18
|
+
a certain weakness class, not a claim that exploiting CWE-X always produces
|
|
19
|
+
technique T.
|
|
20
|
+
|
|
21
|
+
## Method
|
|
22
|
+
|
|
23
|
+
1. For each CWE commonly seen in ICS advisories, ask: if an adversary
|
|
24
|
+
exploited a vulnerability of this weakness class, what's the most direct
|
|
25
|
+
ATT&CK-for-ICS technique describing what they could then do?
|
|
26
|
+
2. Assign one or more mapped techniques, each with:
|
|
27
|
+
- `confidence`: `high` (the technique's own ATT&CK description names
|
|
28
|
+
this weakness class or a direct synonym), `medium` (a common,
|
|
29
|
+
well-established path but not a definitional match), or `low` (plausible
|
|
30
|
+
but more context-dependent).
|
|
31
|
+
- `rationale`: one sentence, so a reader can judge the mapping rather
|
|
32
|
+
than trust it.
|
|
33
|
+
3. `icsprio.join` keeps only the highest-confidence mapping per technique
|
|
34
|
+
when multiple CWEs on the same CVE point to the same technique, and
|
|
35
|
+
drops any mapped technique that isn't in the currently-fetched ATT&CK
|
|
36
|
+
catalog (so a stale crosswalk entry can never claim a retired ID).
|
|
37
|
+
|
|
38
|
+
## Coverage
|
|
39
|
+
|
|
40
|
+
The initial crosswalk (`icsprio/reference_data/cwe_to_attack_ics.csv`) covers
|
|
41
|
+
roughly the two dozen CWEs most frequently assigned in CISA ICS advisories
|
|
42
|
+
and Vulnrichment records (hard-coded/missing credentials, injection
|
|
43
|
+
families, memory-safety bugs, denial-of-service-prone weaknesses,
|
|
44
|
+
cryptographic/transport weaknesses, and access-control failures). A CWE not
|
|
45
|
+
in the table simply contributes no mapped technique — this is silent by
|
|
46
|
+
design (see NEXT_STEPS.md for surfacing "unmapped CWE" explicitly).
|
|
47
|
+
|
|
48
|
+
## Extending it
|
|
49
|
+
|
|
50
|
+
Add a row to `icsprio/reference_data/cwe_to_attack_ics.csv` with the same five
|
|
51
|
+
columns (`cwe_id, cwe_name, attack_technique_id, attack_technique_name,
|
|
52
|
+
confidence, rationale`) and re-run `icsprio build` — no code change needed.
|
|
53
|
+
`tests/test_attack_mapping.py` confirms the loader and dedup logic handle
|
|
54
|
+
new rows correctly.
|
|
55
|
+
|
|
56
|
+
## Terms of use
|
|
57
|
+
|
|
58
|
+
ATT&CK for ICS content (technique names, IDs, tactics) is used under the
|
|
59
|
+
[ATT&CK Terms of Use](https://attack.mitre.org/resources/terms-of-use/),
|
|
60
|
+
which permits use with attribution to MITRE ATT&CK.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Codebook — `data/processed/icsprio_joined.csv`
|
|
2
|
+
|
|
3
|
+
One row per (advisory, CVE) pair. An advisory with no listed CVE keeps one
|
|
4
|
+
row with `cve` empty.
|
|
5
|
+
|
|
6
|
+
| Column | Type | Source | Definition |
|
|
7
|
+
|---|---|---|---|
|
|
8
|
+
| `advisory_id` | string | CISA CSAF | Advisory tracking ID, e.g. `ICSA-26-076-03`. |
|
|
9
|
+
| `advisory_type` | string | derived | `ICSA` (general ICS/OT) or `ICSMA` (medical device), from the ID prefix. |
|
|
10
|
+
| `title` | string | CISA CSAF | Advisory document title. |
|
|
11
|
+
| `published` | date | CISA CSAF | `document.tracking.initial_release_date`. |
|
|
12
|
+
| `updated` | date | CISA CSAF | `document.tracking.current_release_date`. |
|
|
13
|
+
| `vendors` | list[string] | CISA CSAF | Vendor name(s) from the advisory's product tree. |
|
|
14
|
+
| `products` | list[string] | CISA CSAF | Affected product name(s). |
|
|
15
|
+
| `advisory_url` | string | derived | Human-readable advisory page. |
|
|
16
|
+
| `cve` | string | CISA CSAF | The CVE ID this row is about. |
|
|
17
|
+
| `in_kev` | bool | CISA KEV | Whether `cve` appears in the KEV catalog at fetch time. |
|
|
18
|
+
| `kev_date_added` | date | CISA KEV | Date CISA added this CVE to KEV, if applicable. |
|
|
19
|
+
| `kev_due_date` | date | CISA KEV | Federal remediation due date (Binding Operational Directive scope), if applicable. |
|
|
20
|
+
| `kev_ransomware_use` | string | CISA KEV | `Known` or `Unknown` — known use in ransomware campaigns. |
|
|
21
|
+
| `epss_score` | float [0,1] | FIRST EPSS | Estimated 30-day exploitation probability, at fetch time. |
|
|
22
|
+
| `epss_percentile` | float [0,1] | FIRST EPSS | This CVE's percentile rank among all scored CVEs, at fetch time. |
|
|
23
|
+
| `epss_date` | date | FIRST EPSS | The date EPSS scored this CVE (EPSS is point-in-time — see LIMITATIONS). |
|
|
24
|
+
| `ssvc_exploitation` | categorical | Vulnrichment | CISA-ADP's SSVC Exploitation decision point: `none` / `poc` / `active`. |
|
|
25
|
+
| `ssvc_automatable` | categorical | Vulnrichment | SSVC Automatable decision point: `no` / `yes`. |
|
|
26
|
+
| `ssvc_technical_impact` | categorical | Vulnrichment | SSVC Technical Impact decision point: `partial` / `total`. |
|
|
27
|
+
| `cvss_base_score` | float [0,10] | Vulnrichment | CVSS base score from the CISA-ADP container, when present. |
|
|
28
|
+
| `cvss_vector` | string | Vulnrichment | CVSS vector string. |
|
|
29
|
+
| `cwes` | list[string] | Vulnrichment | CWE weakness ID(s) CISA-ADP assigned to this CVE. |
|
|
30
|
+
| `attack_technique_ids` | list[string] | derived | ATT&CK-for-ICS technique IDs mapped from `cwes` via the heuristic crosswalk — see ATTACK_MAPPING.md. **Not** a MITRE or CISA mapping. |
|
|
31
|
+
| `attack_technique_names` | list[string] | derived | Names matching `attack_technique_ids`. |
|
|
32
|
+
| `priority_score` | float [0,100] | derived | icsprio's own OT-relevant priority score — see SCORING.md. |
|
|
33
|
+
| `priority_band` | categorical | derived | `Critical` / `High` / `Medium` / `Low`, from `priority_score` thresholds in SCORING.md. |
|
|
34
|
+
|
|
35
|
+
## Missing values
|
|
36
|
+
|
|
37
|
+
- `epss_score`/`epss_percentile`/`epss_date` are null when a CVE is too new
|
|
38
|
+
or otherwise unscored by EPSS at fetch time — not zero.
|
|
39
|
+
- `ssvc_*`, `cvss_*`, and `cwes` are null/empty when Vulnrichment has not
|
|
40
|
+
yet enriched that CVE — CISA-ADP enriches a subset of CVEs, prioritizing
|
|
41
|
+
recent and higher-signal ones, not every CVE ever published.
|
|
42
|
+
- `attack_technique_ids` is empty both when a CVE has no CWE assignment and
|
|
43
|
+
when its CWE(s) aren't yet in the crosswalk table — these two cases are
|
|
44
|
+
not currently distinguished in the output; see NEXT_STEPS.md.
|