prototyyppi
Experimental Python reference implementation of CSAF 2.1. Validates CSAF advisory documents against the full three-tier test suite (mandatory, recommended, informative) specified in the CSAF 2.1 standard.
Requires Python 3.11 or later.
Install
pip install prototyyppi
The package name on PyPI is prototyyppi (Finnish: "prototype").
The final stable release will be published as csaf.
Optional extras activate additional subcommands or a faster schema backend:
| Extra | Installs | Enables |
|---|---|---|
tui |
textual |
advisory subcommand — interactive terminal advisory browser |
fast-schema |
jsonschema-rs |
Compiled JSON Schema backend — typically 10–15× faster schema validation |
pip install 'prototyyppi[tui]' # advisory browser
pip install 'prototyyppi[fast-schema]' # fast schema backend
pip install 'prototyyppi[tui,fast-schema]' # both
Manual
The man page provides the full CLI reference.
After installation, place it on your MANPATH:
mkdir -p ~/.local/share/man/man1
cp docs/man/prototyyppi.1 ~/.local/share/man/man1/
man prototyyppi
Quickstart
CLI
Validate a CSAF 2.1 advisory and get a human-readable report:
prototyyppi validate advisory.json
File: advisory.json
Overall: FAIL
[PASS] schema — JSON Schema (CSAF 2.1)
[FAIL] 6.1.1 — Missing Definition of Product ID
/product_tree/product_groups/0/product_ids/0: product id `CSAFPID-9080700` is not defined in product_tree
[PASS] 6.1.2 — Multiple Definition of Product ID
…
Exit code is 0 for a valid document, 1 for invalid, 2 for usage errors.
Validation levels — run additional test tiers:
# Basic: schema + mandatory (6.1.x) — default
prototyyppi validate advisory.json
# Extended: + recommended (6.2.x)
prototyyppi validate --level extended advisory.json
# Full: + informative (6.3.x)
prototyyppi validate --level full advisory.json
Output formats:
# TC-compatible JSON (matches the OASIS test-result schema)
prototyyppi validate --format json advisory.json
# SARIF 2.2 (GitHub code scanning, VS Code SARIF viewer)
prototyyppi validate --format sarif advisory.json > results.sarif
# GitHub-flavored markdown — paste directly into a GitHub issue or PR comment
prototyyppi validate --format markdown advisory.json
Batch validation:
# All files in a directory
prototyyppi validate advisories/*.json
# Recursive glob (quote to let Python expand it — avoids shell ARG_MAX limits)
prototyyppi validate 'advisories/**/*.json'
# Batch markdown for a GitHub issue
prototyyppi validate --format markdown 'advisories/*.json'
Skip rules — suppress specific tests by ID or from a YAML file:
prototyyppi validate --skip-rules 6.1.9 advisory.json
prototyyppi validate --skip-rules skip.yaml advisory.json
Suppress output:
prototyyppi validate --quiet advisory.json # hide passing rules
prototyyppi validate --silent advisory.json # exit code only
Performance instrumentation:
# Per-rule timing and peak RSS delta — report to stderr
prototyyppi validate --perf advisory.json
# Save a JSON baseline for later comparison
prototyyppi validate --level full --perf --perf-format json \
--perf-output baseline.json advisory.json
Exit code is unchanged by --perf.
On Windows the RSS delta column shows n/a (the resource module is POSIX-only);
wall-clock timing works on all platforms.
Advisory browser (TUI):
# Open an advisory in the interactive terminal browser
prototyyppi advisory advisory.json
# Multiple files — navigate with arrow keys in the sidebar
prototyyppi advisory advisories/*.json
# Pipe from the producer API
python build_advisory.py | prototyyppi advisory
Requires pip install 'prototyyppi[tui]'.
Four tabs: Summary (metadata and conformance status), Validation (rule-by-rule table),
Dimensions (Appendix C soft-limit report), Content (full structured rendering).
Rule catalog:
prototyyppi info rules # full list
prototyyppi info rules --only-groups mandatory # filter by tier
prototyyppi info rules --format json # machine-readable
Spec ID Group Status Title
--------------------------------------------------------------------------------
2.1 6.1.1 mandatory implemented Missing Definition of Product ID
2.1 6.1.2 mandatory implemented Multiple Definition of Product ID
…
61 rule(s) in catalog (CSAF 2.1).
Environment info:
prototyyppi info env # CSAF support, catalogs, interpreter, platform
prototyyppi info env --level extended # + runtime config, paths, host OS identity
prototyyppi info env --level full # + interpreter detail, flags, CPU, resource usage
prototyyppi info env --format json # machine-readable
For a condensed single-page reference see the Quickstart guide. For tutorials covering validation and document production see the Tutorial index.
Python API — consumer (validate)
from prototyyppi import validate, validate_file
# From a file path
report = validate_file("advisory.json")
# From an already-parsed dict
import msgspec
doc = msgspec.json.decode(open("advisory.json", "rb").read())
report = validate(doc, path="advisory.json")
print("valid:", report.overall_valid)
for result in report.results:
if not result.passed:
print(f"[FAIL] {result.id} — {result.title}")
for err in result.errors:
print(f" {err.instance_path}: {err.message}")
The ValidationReport is a frozen msgspec.Struct.
Pass it to the formatters to render as text, JSON, SARIF, or markdown:
from prototyyppi import to_text, to_tc_json, to_sarif, to_markdown
print(to_text(report))
print(to_tc_json(report))
print(to_sarif(report, version='0.0.0'))
print(to_markdown(report))
Python API — producer (build)
Construct a typed document and call build() to get validated JSON:
from prototyyppi import (
Document, Distribution, FullProductName, Note, Publisher, ProductStatus,
ProductTree, Revision, Tlp, Tracking, Vulnerability,
Metric, MetricContent, cvss31_from_vector, cwe_entry,
)
doc = Document(
category='csaf_security_advisory',
title='Acme Corp — Advisory 2026-001',
publisher=Publisher(category='vendor', name='Acme Corp',
namespace='https://acme.example.com'),
tracking=Tracking(
id='ACME-2026-SA-001', status='final', version='1',
initial_release_date='2026-01-15T10:00:00Z',
current_release_date='2026-01-15T10:00:00Z',
revision_history=[Revision(date='2026-01-15T10:00:00Z', number='1',
summary='Initial release.')],
),
distribution=Distribution(tlp=Tlp(label='CLEAR')),
product_tree=ProductTree(full_product_names=[
FullProductName(name='Widget 1.0', product_id='CSAFPID-W100'),
]),
vulnerabilities=[
Vulnerability(
cve='CVE-2026-10001',
cwes=[cwe_entry('CWE-122')],
notes=[Note(category='description', text='Heap overflow in widget parser.')],
product_status=ProductStatus(known_affected=['CSAFPID-W100']),
metrics=[Metric(
content=MetricContent(
cvss_v3=cvss31_from_vector(
'CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H'
),
),
products=['CSAFPID-W100'],
)],
),
],
)
json_str, report = doc.build() # raises BuildError on structural violations
assert report.overall_valid
import pathlib
pathlib.Path('advisory.json').write_text(json_str, encoding='utf-8')
build() validates the document and returns (json_str, ValidationReport).
The JSON output has sorted keys, two-space indentation, and an auto-injected generator block.
See the producer tutorial for step-by-step coverage of all five profiles, CVSS helpers, and build modes.
URL reachability cache
Rules 6.3.06 and 6.3.07 check that URLs in advisory documents resolve to live endpoints. Because HTTP requests are slow and advisory corpora can be large, the validator provides a four-mode URL cache:
| Mode | Behaviour | When to use |
|---|---|---|
run |
In-memory dedup within the current invocation | Default; interactive use |
disk |
Persist results to disk with a configurable TTL | CI pipelines |
disk-ro |
Read disk cache; skip on cache miss (no network) | Air-gapped or offline builds |
none |
No caching; each URL is checked fresh every run | Debugging |
# Default: in-memory dedup (no flags needed)
prototyyppi validate --level full advisory.json
# Disk cache with default TTL (24h) and default directory (~/.cache/prototyyppi)
prototyyppi validate --level full --url-cache disk advisory.json
# Custom TTL and cache directory
prototyyppi validate --level full \
--url-cache disk \
--url-cache-ttl 7d \
--url-cache-dir /var/cache/prototyyppi \
advisory.json
# Read-only: use cache, skip URL rules on miss (no network calls)
prototyyppi validate --level full --url-cache disk-ro advisory.json
# Disable URL rules entirely (shown as [SKIP]; implies --skip-rules 6.3.06,6.3.07)
prototyyppi validate --level full --no-network advisory.json
TTL accepts: 30m, 6h, 7d (minutes, hours, days).
The disk cache is stored as a JSON file; entries expire individually based on their write time.
Recommended CI pattern:
prototyyppi validate \
--level full \
--url-cache disk \
--url-cache-dir .cache/prototyyppi \
--url-cache-ttl 24h \
'advisories/**/*.json'
Document dimension report (doc-stats)
The doc-stats subcommand measures advisory documents against the informative soft limits
defined in Appendix C of the CSAF 2.1 specification —
file size (§C.1), array lengths (§C.2), and string lengths (§C.3).
It does not perform conformance validation.
Each dimension is rated ok (at or below the caution threshold),
caution (above threshold but within the limit), or exceeds (above the limit).
The overall posture is the worst-case rating across all dimensions.
# Single file — human-readable text
prototyyppi doc-stats advisory.json
# Structured output
prototyyppi doc-stats --format json advisory.json
prototyyppi doc-stats --format yaml advisory.json
# SARIF 2.2 — for GitHub code scanning or VS Code SARIF viewer
prototyyppi doc-stats --format sarif 'advisories/**/*.json' > doc-stats.sarif
# Batch — text report with grand total footer
prototyyppi doc-stats 'advisories/**/*.json'
Adjusting the caution threshold:
# Rate a dimension caution when it exceeds 75% of the limit (default: 50%)
prototyyppi doc-stats --caution-fraction 0.75 advisory.json
Selecting the taxonomy version:
# v21 (default) or its alias v21csd02
prototyyppi doc-stats --spec-version v21csd02 advisory.json
Exit codes: 0 (all ok or caution), 1 (any exceeds, or a file could not be read), 2 (usage error).
The four official OASIS Appendix examples (example/appendix/) all pass at ok posture.
See example/README.md for the full per-file dimension reports.
Single-pass combined conformance + dimension report:
--with-doc-stats on validate runs both pipelines on the same decoded document —
no second parse.
The dimension block is appended to each file's conformance output.
# Single file: conformance + dimensions in one invocation
prototyyppi validate --with-doc-stats advisory.json
# Also fail on any exceeds posture (default: dimension posture does not affect exit code)
prototyyppi validate --with-doc-stats --doc-stats-exit advisory.json
# Machine-readable output with embedded doc_stats key
prototyyppi validate --format json --with-doc-stats advisory.json
# SARIF with two runs per file (validation run + doc-stats run)
prototyyppi validate --format sarif --with-doc-stats advisory.json
Skip rules
Rules can be suppressed individually or via a YAML skip file.
Suppressed rules appear as [SKIP] in the output and do not affect the exit code.
CSV on the command line:
prototyyppi validate --skip-rules 6.1.9,6.2.39.02 advisory.json
# May be specified multiple times (processed left to right)
prototyyppi validate --skip-rules 6.1.9 --skip-rules 6.2.39.02 advisory.json
Re-enable a rule skipped by a previous argument or file (prefix with +):
prototyyppi validate --skip-rules base.yaml --skip-rules +6.1.9 advisory.json
YAML skip file — supports structured entries with reason and expiry:
# skip.yaml
- "schema"
- id: "6.1.9"
reason: "CVSS v3 scorer deviation — under review"
expires: "2026-12-31"
prototyyppi validate --skip-rules skip.yaml advisory.json
Expired entries still take effect; the validator prints a warning to stderr.
Official examples
The example/ directory contains official OASIS CSAF 2.1 advisory examples
from the CSAF TC repository:
six general advisories, four Appendix normative examples (Collapsing Product Paths, examples 11–14),
and thirteen VEX use-case documents.
See example/README.md for per-example validation reports and notes on findings.
To refresh the examples from upstream:
python bin/sync_examples.py # download/update all files
python bin/sync_examples.py --show # list bundled files and sizes
python bin/sync_examples.py --dry-run # compare with upstream without writing
Bundled catalogs
All external reference data is bundled and kept up to date within the package. No network access is required for validation.
| Catalog | Bundled version | Sync script |
|---|---|---|
| CWE | v4.9–v4.13 (969 weaknesses) | python bin/sync_cwe.py |
| SPDX | 3.28.0 + ScanCode licensedb | python bin/sync_spdx.py |
| SSVC | format_version 3 | python bin/sync_ssvc.py |
| CSAF translations | v2.1 (de) | python bin/sync_translations.py |
Life Cycle Management
Four categories of data bundled in the package require periodic maintenance.
| Category | Location | Update trigger |
|---|---|---|
| TC fixture data | csaf/v21/data/ |
New TC test suite release |
| Spec schema files | csaf/v21/schema/ |
Schema normative change |
| Reference catalogs | csaf/v21/{cwe,spdx,ssvc,translations}/ |
Upstream version release |
| Official examples | example/ |
New CSAF TC release (main, appendix, or VEX) |
TC fixture data
The test suite reads fixture files from
prototyyppi/csaf/v21/data/{mandatory,recommended,informative}/.
These are a snapshot of the OASIS CSAF TC test suite
and require a sibling checkout of the TC repository to refresh.
make sync-fixtures csaf_tc_root=../csaf-2.1 # default: csaf_tc_root=../csaf-2.1
After syncing, stage and commit the changed files:
fossil add prototyyppi/csaf/v21/data/
fossil commit -m "sync: TC fixture data YYYY-MM-DD"
CWE catalog
The CWE catalog at prototyyppi/csaf/v21/cwe/catalog.json is built
from MITRE's XML distribution.
Download the desired CWE versions and run:
python bin/sync_cwe.py --latest-version 4.13 \
cwec_v4.9.xml cwec_v4.10.xml cwec_v4.13.xml cwec_v4.14.xml cwec_v4.20.xml
The script reads only local XML files; no network access is required.
--latest-version sets the version marker used by rule 6.2.24
(defaults to the highest version found in the supplied files when omitted).
SPDX license catalog
The SPDX catalog at prototyyppi/csaf/v21/spdx/catalog.json combines
the SPDX license list with the ScanCode license database.
Network (default):
python bin/sync_spdx.py
Air-gapped — supply locally downloaded source files:
python bin/sync_spdx.py \
--spdx-licenses licenses.json \
--spdx-exceptions exceptions.json \
--scancode scancode-index.json
Download sources from the SPDX license list JSON API and the ScanCode licensedb index.
SSVC decision point catalog
The SSVC catalog at prototyyppi/csaf/v21/ssvc/catalog.json is built
from the CERTCC/SSVC repository.
python bin/sync_ssvc.py --sync ssvc --sync cvss # sync both namespaces
python bin/sync_ssvc.py --show # print current catalog
python bin/sync_ssvc.py --namespace ssvc --key A --latest-version 3.0.0 # manual override
Syncing requires network access to the GitHub Contents API.
For air-gapped environments, review the upstream repository offline
and apply changes with --namespace / --key / --latest-version.
CSAF translations catalog
The translations catalog at prototyyppi/csaf/v21/translations/translations.json
is maintained by the OASIS CSAF TC.
python bin/sync_translations.py # fetch and write
python bin/sync_translations.py --show # print current catalog
python bin/sync_translations.py --dry-run # compare with upstream without writing
OASIS examples
The official OASIS CSAF 2.1 examples are bundled in example/ (main advisories),
example/appendix/ (Appendix normative examples), and example/csaf_vex/ (VEX use cases).
All three groups are refreshed via bin/sync_examples.py.
python bin/sync_examples.py # download/refresh all groups
python bin/sync_examples.py --show # print bundled filenames and sizes
python bin/sync_examples.py --dry-run # compare with upstream without writing
python bin/sync_examples.py --no-appendix # skip appendix/ group
python bin/sync_examples.py --no-vex # skip csaf_vex/ group
After syncing, stage and commit the changed files:
fossil add example/
fossil commit -m "sync: OASIS CSAF examples YYYY-MM-DD"
Adding a new bundled catalog
To add a new reference catalog following the established pattern:
- Create
bin/sync_<name>.pywith network and (where possible) air-gap modes. - Create
prototyyppi/csaf/v21/<name>/and add the generated JSON file. - Add
package-dataglobs topyproject.toml. - Add an
lru_cacheloader inprototyyppi/csaf/v21/rules/_shared.py. - Add a section provider in
prototyyppi/_env.pyforinfo envoutput. - Add the catalog to the "Bundled catalogs" table in this README.
Fuzzing
Coverage-guided fuzz testing uses AFL via python-afl. Seven harnesses cover the parser and filter functions with the highest attack surface.
| Target | Harness | Functions covered |
|---|---|---|
cvss-vector |
fuzz/fuzz_cvss_vector.py |
_parse_cvss_vector, _cvss2/3/4_base_score |
vers |
fuzz/fuzz_vers.py |
_parse_vls, _get_vers_pairs |
purl |
fuzz/fuzz_purl.py |
_validate_purl, _purl_base, _purl_version |
spdx-expr |
fuzz/fuzz_spdx_expr.py |
_parse_spdx_expression |
only-groups |
fuzz/fuzz_only_groups.py |
_filter_by_groups |
skip-rules |
fuzz/fuzz_skip_rules.py |
_build_skip_rules (CSV branch) |
ttl |
fuzz/fuzz_ttl.py |
_parse_ttl |
Prerequisites: afl-fuzz and python-afl must be installed and on PATH.
On macOS with pyenv, ensure the virtualenv is active so PYTHON_BIN resolves to
the real interpreter (not the pyenv shim).
# Run all targets sequentially (default: 60 s each)
make fuzz
# Run a single target
make fuzz-ttl
# Adjust the time budget
make fuzz-cvss-vector fuzz_time=300
# After a run, promote AFL-discovered queue entries into the seed corpus
make fuzz-update-seeds
# Review fuzz/seeds/ and commit any valuable new seeds
Crash and hang findings land in fuzz/findings/<target>/.
The seed corpus lives in fuzz/seeds/<target>/.
Each target's seed directory contains at least one valid, non-crashing input
so AFL can establish a baseline before mutation begins.
Design and requirements
| Document | Identifier | File |
|---|---|---|
| Software Requirements Specification | PRO-SRS-001 | docs/requirements/srs/ |
| Software Design Description | PRO-SDD-001 | docs/design/sdd/ |
Both documents follow the MIL-STD-498 DID structure and are rendered into the documentation site alongside the quickstart, tutorial, and example gallery.
Bug Tracker
Feature requests and bug reports go to the todos of prototyyppi.
Primary Source repository
The main source of prototyyppi is on a mountain in Central Switzerland under
configuration control (fossil).
Contributions
To share small changes under the repository's license, kindly send a patchset per email using git send-email.
Support
Submit issues at https://todo.sr.ht/~sthagen/prototyyppi or write plain text email to ~sthagen/prototyyppi@lists.sr.ht.
Security Policy
See SECURITY.md for the security policy.
Changes
See docs/releases/ for release summaries and docs/releases/changes/ for the detailed change log.
Coverage
The test suite maintains high branch coverage (≥99%).
The HTML report (if generated) is in site/coverage/.
SBOM
Runtime dependency information is published in docs/sbom/ in SPDX 3.0 (JSON-LD)
and CycloneDX 1.6 (JSON) formats.
See docs/sbom/README.md for the component inventory and validation guide.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file prototyyppi-2026.7.29.tar.gz.
File metadata
- Download URL: prototyyppi-2026.7.29.tar.gz
- Upload date:
- Size: 340.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bd971d081e8e3dda06360716d1a629d5de03d7989a31a5d304833a8c70d90006
|
|
| MD5 |
fe342516344d8fdb92e5d59b0ee740fa
|
|
| BLAKE2b-256 |
d0f2fe33bd63227ed230c4529c464f32e2ad532e208ccf6bc488a770f9f0b371
|
File details
Details for the file prototyyppi-2026.7.29-py3-none-any.whl.
File metadata
- Download URL: prototyyppi-2026.7.29-py3-none-any.whl
- Upload date:
- Size: 961.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
846657d88efa348104f5d92397282c8331b0ab774ed373fdf061d5dcf654ac87
|
|
| MD5 |
b258b16b8d642358d3df82ecc809bab4
|
|
| BLAKE2b-256 |
908764fd15b332d7c5563a9671022aa8790ab96b3c44cf2b5fc0354f44d89c84
|