A modern Flask application for browsing, curating, and publishing CPE (Common Platform Enumeration) data with a built-in moderation workflow, API access, and dataset portability.
- Fast catalog browsing for vendors, products, and CPE entries.
- Powerful search across names, titles, versions, and full CPE URIs.
- Structured proposals for edits, additions, and relationship changes.
- Moderation dashboard for reviewing and approving submitted changes.
- Approved change feed with dedicated pages plus RSS/Atom endpoints.
- Read-only OpenAPI interface with interactive Swagger docs.
- NVD feed ingestion for both CPE and CPE Match archives.
- PURL → CPE mapping import (
import-purl2cpe) with optional replace mode and auto-creation of missing vendor/product/CPE records. - GCVE enriched CVE dump import (
import-gcve-enriched-cves) for CVE→CPE references plus missing vendor/product/CPE records. - CPE vulnerability reference proposals (CVE/GCVE/GHSA +
cpeApplicability) in the public contribution workflow. - Entity note proposals for adding structured context without directly editing canonical records.
- Portable dataset export/import to migrate curated data between instances.
- Stable deterministic UUIDs for vendor and product identity consistency.
- Browse vendors, products, and CPE records.
- Drill into vendor/product detail pages and linked relationships.
- Submit anonymous proposals to:
- edit an existing CPE
- add a CPE to an existing vendor/product
- add a product to an existing vendor
- add a new vendor and product
- propose a CPE vulnerability reference (CVE/GCVE/GHSA + cpeApplicability)
- attach note proposals to entities
- Explore the approved change history and detail pages.
- Admin login and protected review workflows.
- Accept/reject proposal queue with server-side application of accepted updates.
- Optional request rate limiting for anonymous submissions.
- Built-in CSRF token handling for form submissions.
- SQLite optimization defaults (WAL mode, busy timeout, reindex support).
- OpenAPI spec endpoint and Swagger UI.
- Search and pagination for API list endpoints.
- Vendor/product suggestion endpoints for picker-style UIs.
- Import/export commands for offline transfer and environment bootstrapping.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
python -m flask --app run init-db --drop
python run.pyThen open the local URL printed by Flask.
Use the dedicated WSGI entry point with a production WSGI server instead of the Flask debug server:
./start.pyThe start script runs Gunicorn against wsgi:app and binds to all IPv4 and IPv6 addresses on PORT (default 8000). You can pass additional Gunicorn options after the script name:
PORT=8000 ./start.py --workers 4The wsgi.py entry point creates the Flask application without enabling debug mode.
DATABASE_URLSECRET_KEYADMIN_USERNAMEADMIN_PASSWORDPROPOSAL_RATE_LIMIT_PER_HOUR(default10; set0or negative to disable)SQLITE_BUSY_TIMEOUT_SECONDS(default30)
- Home:
GET / - Vendors:
GET /vendors - Vendor detail:
GET /vendors/{vendor_uuid} - Product detail:
GET /products/{product_uuid} - CPE detail:
GET /cpes/{cpe_id} - Statistics:
GET /statistics - Proposal form:
GET|POST /proposals/new - Note proposal form:
GET|POST /proposals/note/new - Approved changes:
GET /changes - Approved change detail:
GET /changes/{proposal_id} - RSS feed:
GET /changes.rss - Atom feed:
GET /changes.atom - Vendor change feeds:
GET /vendors/{vendor_uuid}/changes.rss,GET /vendors/{vendor_uuid}/changes.atom - Product change feeds:
GET /products/{product_uuid}/changes.rss,GET /products/{product_uuid}/changes.atom - CPE change feeds:
GET /cpes/{cpe_id}/changes.rss,GET /cpes/{cpe_id}/changes.atom
- Swagger UI:
GET /api/docs - OpenAPI spec:
GET /api/openapi.yaml
Documented endpoints include:
GET /api/vendorsGET /api/vendors/{vendor_uuid}GET /api/vendors/suggestGET /api/products/{product_uuid}GET /api/products/suggestGET /api/changesGET /api/changes/{proposal_id}GET /api/cpesGET /api/cpes/{cpe_id}GET /api/cpes/{cpe_id}/vulnerability-referencesGET /api/vulnerability-references
curl "http://127.0.0.1:5000/api/vendors?page=1&per_page=5"
curl "http://127.0.0.1:5000/api/cpes?vendor_q=microsoft&part=a&per_page=10"
curl "http://127.0.0.1:5000/api/changes?per_page=10"A focused API regression suite is included under tests/ and uses a very small CPE fixture (tests/fixtures/cpe_subset.json) representing a tiny subset of real-world CPE-style records (vendors, products, CPE URIs, and vulnerability links).
Run locally:
pytest -qCI runs this same suite via .github/workflows/api-tests.yml.
Initialize schema:
python -m flask --app run init-dbReset schema/data:
python -m flask --app run init-db --dropAdd missing columns without dropping existing rows:
python -m flask --app run init-db --alterRebuild indexes (and planner stats by default):
python -m flask --app run reindex-dbSkip ANALYZE during reindex:
python -m flask --app run reindex-db --no-analyzepython -m flask --app run import-nvd-cpesFrom a local archive:
python -m flask --app run import-nvd-cpes --source /path/to/nvdcpe-2.0.tar.gzReplace existing vendor/product/CPE data first:
python -m flask --app run import-nvd-cpes --replacepython -m flask --app run import-nvd-cpematchesFrom a local archive:
python -m flask --app run import-nvd-cpematches --source /path/to/nvdcpematch-2.0.tar.gzImport mappings from a local clone of scanoss/purl2cpe.
By default, the command expects the repository at ../purl2cpe relative to cpe-editor.
If a CPE from purl2cpe does not exist locally yet, the importer will create the vendor, product, and CPE entry first.
python -m flask --app run import-purl2cpeUse a custom clone path:
python -m flask --app run import-purl2cpe --source /path/to/purl2cpeReplace existing CPE↔PURL mappings first:
python -m flask --app run import-purl2cpe --replaceImport CVE→CPE vulnerability references from a local clone of CVEProject/gcve-enriched-dumps.
By default, the command expects the repository at ../gcve-enriched-dumps relative to cpe-editor. The source can point either at the repository root or directly at its cves directory.
If an imported CPE does not exist locally yet, the importer will create the vendor, product, and CPE entry first. Existing CVE→CPE references are left in place and duplicate references are skipped.
python -m flask --app run import-gcve-enriched-cvesUse a custom clone or cves directory path:
python -m flask --app run import-gcve-enriched-cves --source /path/to/gcve-enriched-dumpsCommit progress after a custom number of CVE files:
python -m flask --app run import-gcve-enriched-cves --batch-size 1000Export vendors, products, relationships, and CPE entries:
python -m flask --app run export-app-datasetCustom output path:
python -m flask --app run export-app-dataset --output /path/to/cpe-editor-dataset.tar.gzInclude proposals:
python -m flask --app run export-app-dataset --include-proposalsConvert a portable app export into a deterministic directory tree that can be committed to a git repository without rewriting one large JSON blob on every change:
python tools/export_dataset_to_git.py /path/to/cpe-editor-dataset.tar.gz /path/to/git-datasetIf the destination already exists, the tool updates the generated files in place
without deleting the destination directory, so repository metadata such as .git
is preserved. Stale generated files are removed from the managed dataset paths.
The generated layout includes manifest.json, one JSON file per vendor/product,
sharded CPE JSON Lines files under cpes/<vendor>/<product>/<part>.jsonl, and
separate JSON Lines shards for metadata, relationships, PURL mappings, and
proposals.
Convert the same portable app export into one newline-delimited JSON file for streaming data pipelines or tools that prefer a single append-style file:
python tools/export_dataset_to_jsonl.py /path/to/cpe-editor-dataset.tar.gz /path/to/cpe-editor-dataset.ndjsonThe converter accepts either the .tar.gz export or a plain dataset.json file.
It parses each top-level record array incrementally and writes one NDJSON row per
record, so memory usage is bounded by the largest individual dataset record
instead of the full export size. The first row contains dataset metadata; later
rows have type: "record", collection, and record fields.
python -m flask --app run import-app-dataset --source /path/to/cpe-editor-dataset.tar.gzReplace existing data first:
python -m flask --app run import-app-dataset --source /path/to/cpe-editor-dataset.tar.gz --replaceSkip proposal history while importing:
python -m flask --app run import-app-dataset --source /path/to/cpe-editor-dataset.tar.gz --replace --no-proposals- Vendor UUIDs are deterministic UUIDv5 values based on normalized vendor names.
- Product UUIDs are deterministic UUIDv5 values based on normalized vendor+product names.
- Repeated imports keep identity stable across compatible instances.
A .gitignore is included to exclude common local artifacts like:
- SQLite files under
instance/ - virtual environments
- Python cache files
- local import/export archives and
.envfiles