Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions .github/workflows/datasets-validate-all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ name: datasets-validate-all

# Runs data_validation/run_all_checks.py: every dataset's own validate.py
# (including ones in sub-collections), then the shared checks in
# data_validation/validators/ on every data file. Then the data_validation
# test suite, and the cross-dataset consistency check. The coverage report
# and anomaly list go to the job summary for reviewers; they never fail
# the job.
# data_validation/validators/ on every data file, and the cross-dataset
# consistency check. The tests job runs the data_validation test suite on
# each supported Python. The coverage report and anomaly list go to the job
# summary for reviewers; they never fail the job.

on:
push:
branches: [main]
Expand Down Expand Up @@ -43,9 +44,6 @@ jobs:
- name: Cross-dataset consistency
run: python data_validation/qc_tools/consistency_check.py

- name: data_validation tests
run: python -m pytest data_validation/tests -v

- name: Coverage report and anomalies (job summary)
if: always()
run: |
Expand All @@ -58,3 +56,25 @@ jobs:
python data_validation/qc_tools/anomaly_detector.py
echo '```'
} >> "$GITHUB_STEP_SUMMARY"

# The tooling's own tests on every Python release it supports.
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.11', '3.12', '3.13']
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}

- name: Install requirements
run: python -m pip install --quiet -r data_validation/requirements.txt

- name: data_validation tests
run: python -m pytest data_validation/tests -v
69 changes: 69 additions & 0 deletions .github/workflows/reference-data-refresh.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: reference-data-refresh

# Monthly: regenerate the MITRE ATLAS and CWE tables in
# data_validation/reference_data/ from MITRE's latest releases. If anything
# changed, push the result to a branch and open an issue so a maintainer can
# open the PR (this repository does not let workflows open pull requests).
on:
schedule:
- cron: '17 6 2 * *' # 06:17 UTC on the 2nd of each month
workflow_dispatch:

permissions:
contents: write
issues: write

jobs:
refresh:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Set up Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.11'

- name: Install requirements
run: python -m pip install --quiet -r data_validation/requirements.txt

- name: Regenerate ATLAS and CWE tables
run: python data_validation/reference_data/update_reference_data.py

- name: Run the checks against the new tables
id: checks
continue-on-error: true
run: python data_validation/run_all_checks.py --no-shared && python data_validation/validators/crossref_validator.py

- name: Push a branch and open an issue if anything changed
env:
GH_TOKEN: ${{ github.token }}
CHECKS: ${{ steps.checks.outcome }}
run: |
if git diff --quiet -- data_validation/reference_data; then
echo "Reference data is current."
exit 0
fi
branch="automation/reference-data-$(date -u +%Y-%m)"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -c "$branch"
git add data_validation/reference_data
git commit -m "reference_data: refresh MITRE ATLAS and CWE tables"
git push --force origin "$branch"
versions=$(grep -E '^\| (MITRE ATLAS|CWE) ' data_validation/reference_data/SOURCES.md | cut -d'|' -f2,4)
{
echo "The monthly refresh regenerated the MITRE ATLAS and CWE tables."
echo
echo "Versions: $versions"
echo
echo "Identifier check against the existing data: **$CHECKS**. A failure usually means MITRE retired or renamed an ID the datasets cite; the crossref output in the run log names the entries."
echo
echo "Open the PR: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/main...${branch}?expand=1"
echo
echo "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
} > body.md
if gh issue list --state open --search "in:title Reference data refresh $(date -u +%Y-%m)" --json number --jq 'length' | grep -q '^0$'; then
gh issue create --title "Reference data refresh $(date -u +%Y-%m)" --body-file body.md
fi
9 changes: 5 additions & 4 deletions data_validation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ The tooling has two jobs: keep the research data behind OWASP deliverables (the
|---|---|---|
| `schema_validator.py` | Schema violations, for files no dataset validator covers | File with no schema to check against |
| `dsgai_mapping_check.py` | DSGAI ID not in the taxonomy; a mapping value that is not a DSGAI ID | Same ID twice in one mapping; more than 6 mappings |
| `crossref_validator.py` | Malformed CVE/GHSA/CWE/ATLAS ID; CVE year in the future; unknown CWE or ATLAS ID | Deprecated CWE; retired ATLAS ID; malformed ID in free text (often a placeholder) |
| `crossref_validator.py` | Malformed CVE/GHSA/CWE/ATLAS ID; CVE year in the future; unknown CWE, ATLAS or OWASP Top 10 ID (`LLM01:2026`, `ASI01:2026`) | Deprecated CWE; retired ATLAS ID; OWASP ID without its edition year; malformed ID in free text (often a placeholder) |
| `severity_check.py` | CVSS v3.x/v4.0 vector that doesn't parse; score that doesn't match its vector; qualitative rating outside the score's FIRST band | Score with no vector |
| `anonymization_scanner.py` | — | Email addresses, keys and tokens, private or (in a network context) public IPs, internal hostnames, home paths, SSNs |
| `dedup_checker.py` | Two records in one dataset with the same ID | Records with identical or ≥ 80% similar text |

Expand Down Expand Up @@ -75,7 +76,7 @@ After the automated checks pass, contributions are reviewed by cybersecurity and

## Running the checks

Python 3.10 or higher.
Python 3.11 or higher (CI tests 3.11, 3.12 and 3.13).

```bash
pip install -r data_validation/requirements.txt
Expand Down Expand Up @@ -111,13 +112,13 @@ data_validation/
├── qc_tools/ Reports for reviewers
├── schemas/ Schemas for datasets without their own schema.json
│ (the rest are pointers to the dataset-local schemas)
├── reference_data/ DSGAI, MITRE ATLAS and CWE lookup tables; see SOURCES.md
├── reference_data/ DSGAI, MITRE ATLAS, CWE and OWASP Top 10 lookup tables; see SOURCES.md
└── tests/ Tests and deliberately valid/invalid fixtures
```

Dataset-local schemas (`datasets/<name>/schema.json`) are authoritative. `schemas/exploit`, `vulnerability`, `riskassessment` and `agentdataflow_trace` are kept only as `$ref` pointers to them; `schemas/incident`, `rag`, `crossframework_mapping` and `promptinj_testcase` are the only schemas for their data.

The ATLAS and CWE tables are generated from MITRE's releases by `reference_data/update_reference_data.py`; don't edit them by hand.
The ATLAS and CWE tables are generated from MITRE's releases by `reference_data/update_reference_data.py`; don't edit them by hand. The `reference-data-refresh` workflow runs it on the 2nd of each month and, when MITRE has published something new, pushes a branch and opens an issue for a maintainer to turn into a PR. `owasp_top10.csv` is maintained by hand when OWASP publishes a new edition.

---

Expand Down
10 changes: 5 additions & 5 deletions data_validation/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ The data validation pipeline is a set of Python scripts that check contributed d

## Prerequisites

### Python 3.10+
### Python 3.11+

The scripts require Python 3.10 or higher. Most modern systems have Python pre-installed.
The scripts require Python 3.11 or higher. Most modern systems have Python pre-installed.

**Check if you have it:**

```bash
python3 --version
```

If you see `Python 3.10.x` or higher, you're good. If not:
If you see `Python 3.11.x` or higher, you're good. If not:

| Platform | How to install |
|---|---|
Expand Down Expand Up @@ -95,7 +95,7 @@ You'll know it's active when your terminal prompt changes to show `(venv)` at th
pip install -r requirements.txt
```

> **What this does:** Reads the `requirements.txt` file and installs all the Python libraries the validation scripts need. That is `jsonschema` (schema validation) and `pytest` (the tests), plus `pyyaml`, which only the reference-data refresh script uses.
> **What this does:** Reads the `requirements.txt` file and installs all the Python libraries the validation scripts need. That is `jsonschema` with its format checkers (schema validation, including dates and URIs) and `pytest` (the tests), plus `pyyaml`, which only the reference-data refresh script uses.

**If you get a permissions error:** Make sure your virtual environment is activated (Step 2). If you're not using a virtual environment, add `--user` to the command: `pip install --user -r requirements.txt`

Expand Down Expand Up @@ -169,7 +169,7 @@ Python might be installed as `python` instead of `python3` on your system (commo
python --version
```

If that shows 3.10+, use `python` everywhere this guide says `python3`.
If that shows 3.11+, use `python` everywhere this guide says `python3`.

### "No module named 'jsonschema'" (or any other module)

Expand Down
3 changes: 3 additions & 0 deletions data_validation/reference_data/SOURCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ Lookup tables the validators check identifiers against.
|---|---|---|---|
| DSGAI | `dsgai_entries.json` | 1.0 | OWASP GenAI Data Security Risks and Mitigations 2026 (kept identical to `datasets/_shared/dsgai_taxonomy.json`; a test enforces it) |
| MITRE ATLAS | `mitre_atlas_techniques.csv` | 2026.09 | latest release listed in `https://raw.githubusercontent.com/mitre-atlas/atlas-data/main/dist/manifest.yaml` |
| OWASP Top 10s | `owasp_top10.csv` | LLM 2025, LLM 2026, Agentic 2026 | LLM: `https://github.com/GenAI-Security-Project/GenAI-LLM-Top10` (2026 published 2026-08-04). Agentic: `https://genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/` (published 2025-12-09) |
| CWE | `cwe_ids.csv` | 4.20 | `https://cwe.mitre.org/data/xml/cwec_latest.xml.zip` (weaknesses, categories and views) |

`owasp_top10.csv` is maintained by hand. The 2026 LLM list renumbered the entries (LLM03 was Supply Chain in 2025 and is Excessive Agency in 2026), so IDs always carry the edition year; `equivalent_2025` gives the 2025 ID each 2026 entry continues (LLM08:2026 Hidden Context Exposure broadens LLM07:2025 System Prompt Leakage). OWASP writes the Agentic IDs without a year (ASI01); this repository adds `:2026` so a future edition cannot collide.

Regenerate the ATLAS and CWE tables with `python update_reference_data.py`; it fills in the Version column. Do not edit those two CSVs by hand.

`datasets/_shared/dsgai_taxonomy.json` is the DSGAI list the validators use. `dsgai_entries.json` is kept because the Turkish prompt-injection validator reads it.
Expand Down
31 changes: 31 additions & 0 deletions data_validation/reference_data/owasp_top10.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
id,list,edition,name,equivalent_2025
LLM01:2025,LLM,2025,Prompt Injection,
LLM02:2025,LLM,2025,Sensitive Information Disclosure,
LLM03:2025,LLM,2025,Supply Chain,
LLM04:2025,LLM,2025,Data and Model Poisoning,
LLM05:2025,LLM,2025,Improper Output Handling,
LLM06:2025,LLM,2025,Excessive Agency,
LLM07:2025,LLM,2025,System Prompt Leakage,
LLM08:2025,LLM,2025,Vector and Embedding Weaknesses,
LLM09:2025,LLM,2025,Misinformation,
LLM10:2025,LLM,2025,Unbounded Consumption,
LLM01:2026,LLM,2026,Prompt Injection,LLM01:2025
LLM02:2026,LLM,2026,Sensitive Information Disclosure,LLM02:2025
LLM03:2026,LLM,2026,Excessive Agency,LLM06:2025
LLM04:2026,LLM,2026,Supply Chain,LLM03:2025
LLM05:2026,LLM,2026,Data and Model Poisoning,LLM04:2025
LLM06:2026,LLM,2026,Unbounded Consumption,LLM10:2025
LLM07:2026,LLM,2026,Misinformation,LLM09:2025
LLM08:2026,LLM,2026,Hidden Context Exposure,LLM07:2025
LLM09:2026,LLM,2026,Vector and Embedding Weaknesses,LLM08:2025
LLM10:2026,LLM,2026,Improper Output Handling,LLM05:2025
ASI01:2026,ASI,2026,Agent Goal Hijack,
ASI02:2026,ASI,2026,Tool Misuse and Exploitation,
ASI03:2026,ASI,2026,Identity and Privilege Abuse,
ASI04:2026,ASI,2026,Agentic Supply Chain Vulnerabilities,
ASI05:2026,ASI,2026,Unexpected Code Execution,
ASI06:2026,ASI,2026,Memory and Context Poisoning,
ASI07:2026,ASI,2026,Insecure Inter-Agent Communication,
ASI08:2026,ASI,2026,Cascading Failures,
ASI09:2026,ASI,2026,Human-Agent Trust Exploitation,
ASI10:2026,ASI,2026,Rogue Agents,
4 changes: 3 additions & 1 deletion data_validation/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Validators, QC tools and their tests
jsonschema>=4.18.0
# [format-nongpl] adds the checkers for "uri" and other formats; without it
# jsonschema treats those formats as annotations and never fails them.
jsonschema[format-nongpl]>=4.18.0
pytest>=7.0.0
# Only for reference_data/update_reference_data.py (refreshing the ATLAS table)
pyyaml>=6.0
7 changes: 5 additions & 2 deletions data_validation/run_all_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
silently passing.
2. Shared checks (data_validation/validators/) on every data file:
schema (for files no dataset validator covers), DSGAI IDs, CVE/CWE/ATLAS
IDs, anonymization patterns and duplicates. Errors fail the run;
and OWASP Top 10 IDs, CVSS scores and ratings, anonymization patterns
and duplicates. Errors fail the run;
heuristic warnings (anonymization, near-duplicates, retired IDs) are
counted, and printed with --verbose.

Expand All @@ -34,6 +35,7 @@

from validators import ( # noqa: E402
anonymization_scanner, crossref_validator, dedup_checker, dsgai_mapping_check, schema_validator,
severity_check,
)
from validators._common import ( # noqa: E402
ERROR, SKIP_DIRS, Finding, ensure_utf8_stdout, iter_data_files, load_json, load_taxonomy_ids,
Expand Down Expand Up @@ -100,6 +102,7 @@ def shared_checks(dataset: Path, datasets_root: Path) -> tuple[int, list[Finding
findings += schema_validator.validate_file(path, dataset_root=dataset)
findings += dsgai_mapping_check.check_data(path, data, taxonomy)
findings += crossref_validator.check_data(path, data)
findings += severity_check.check_data(path, data)
findings += anonymization_scanner.check_data(path, data)
records, load_errors = dedup_checker.load_records(files)
findings += load_errors + dedup_checker.check_records(records)
Expand Down Expand Up @@ -162,7 +165,7 @@ def main(argv: list[str] | None = None) -> int:

checked_files = errors = warnings = 0
if not args.no_shared:
print("\nShared checks (schema, DSGAI IDs, CVE/CWE/ATLAS IDs, anonymization, duplicates):")
print("\nShared checks (schema, DSGAI, CVE/CWE/ATLAS/OWASP IDs, CVSS, anonymization, duplicates):")
for dataset in datasets:
n, findings = shared_checks(dataset, datasets_root)
if not n:
Expand Down
2 changes: 1 addition & 1 deletion data_validation/schemas/crossframework_mapping.schema.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Cross-Framework Control Mapping Entry",
"type": "object",
"required": ["dsgai_id", "dsgai_name", "framework", "control_id", "control_name", "relationship"],
Expand Down
2 changes: 1 addition & 1 deletion data_validation/schemas/incident.schema.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Incident Dataset Entry",
"type": "object",
"required": ["incident_id", "date", "summary", "dsgai_mapping", "attack_vector", "data_types_affected", "impact"],
Expand Down
2 changes: 1 addition & 1 deletion data_validation/schemas/promptinj_testcase.schema.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Prompt Injection and Data Extraction Test Case",
"type": "object",
"required": ["testcase_id", "category", "dsgai_mapping", "prompt", "expected_secure", "expected_vulnerable", "severity"],
Expand Down
2 changes: 1 addition & 1 deletion data_validation/schemas/rag.schema.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RAG Poisoning and Retrieval Integrity Entry",
"type": "object",
"required": ["document_id", "category", "dsgai_mapping", "content"],
Expand Down

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions data_validation/tests/test_crossref_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,11 @@ def test_reference_tables_are_complete():
assert cr.atlas_status()["AML.T0051"] == "current"
assert len(cr.cwe_status()) > 1000
assert cr.cwe_status()["CWE-79"] != "Deprecated"


def test_owasp_ids():
assert levels({"owasp_llm_top10_mapping": ["LLM03:2025", "LLM04:2026"]}) == []
assert levels({"owasp_agentic_top10_mapping": ["ASI10:2026"]}) == []
assert levels({"notes": "maps to LLM11:2026"}) == [ERROR]
assert levels({"notes": "an old LLM01:2024 reference"}) == [ERROR]
assert levels({"notes": "OWASP LLM03 (Supply Chain)"}) == [WARN]
51 changes: 51 additions & 0 deletions data_validation/tests/test_cvss_score.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Tests for cvss_score.py, the in-house CVSS v3.x and v4.0 scorer."""
from __future__ import annotations

import json
import sys
from pathlib import Path

import pytest

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

from validators.cvss_score import CVSSError, base_score # noqa: E402

FIXTURES = Path(__file__).resolve().parent / "fixtures"


def test_v4_matches_the_first_reference_calculator():
"""500 vectors (base, threat and environmental metrics) scored by cvss40.js."""
reference = json.loads((FIXTURES / "cvss40_reference_scores.json").read_text(encoding="utf-8"))
assert len(reference) == 500
mismatches = [(v, s, base_score(v)) for v, s in reference if base_score(v) != s]
assert mismatches == []


@pytest.mark.parametrize("vector,score", [
# Published NVD/CNA scores
("CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", 8.8),
("CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H", 9.6),
("CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:N", 6.4),
("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N", 9.3),
("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", 7.5),
("CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", 9.8),
("CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:N", 0.0),
("CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H/E:P/RL:O", 9.8), # temporal metrics don't change the base
("CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", 9.3),
])
def test_known_scores(vector, score):
assert base_score(vector) == score


@pytest.mark.parametrize("vector", [
"CVSS:3.1/AV:X/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", # bad value
"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H", # missing A
"CVSS:3.1/AV:N/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", # duplicate
"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N", # missing SA
"CVSS:2.0/AV:N",
"AV:N/AC:L/Au:N/C:P/I:P/A:P",
])
def test_invalid_vectors(vector):
with pytest.raises(CVSSError):
base_score(vector)
Loading
Loading