Gap register produced against codebase HEAD c0072f4, branch feature/pfp-pyrometrix. 10 gaps, 2 at P0, 4 at P1. P0 items must close before first external API access. P1 items must close before EC2 production deployment.
| ID | Severity | Title | Effort | Status |
|---|---|---|---|---|
| GAP-01 | P0 — Critical | 18 unauthenticated routes | 2h | Open |
| GAP-02 | P1 — High | Single shared key, no RBAC | 1d | Open |
| GAP-03 | P0 — Critical | Path traversal in rule file editor | — | Fixed |
| GAP-04 | P1 — High | Rule source-of-truth ambiguity | 4h | Open |
| GAP-05 | P1 — High | No rule audit trail | 1d | Open |
| GAP-06 | P1 — High | No production deployment path | 4h + UAT | Blocked |
| GAP-07 | P2 — Medium | No structured logging | 4h | Open |
| GAP-08 | P2 — Medium | No rate limiting | 2h | Open |
| GAP-09 | P2 — Medium | SQLite no backup or HA | 2h (short) | Open |
| GAP-10 | P3 — Low | No load or penetration testing | 1 week | Open |
Affected files: nexus/api/app.py, nexus/api/routes_rule_editor.py
18 routes are reachable without an API key. Any caller who discovers an endpoint can read all profiles and rules, execute the calculation engine with arbitrary inputs, trigger ingestion pipeline runs against arbitrary data sources, and read raw rule JSON files from disk.
GET /api/profiles — exposes full rule profile index
GET /api/profiles/active — exposes active rule set + formulas
GET /api/profiles/{profile_id} — same
GET /rules — all loaded rules from JSON files
GET /types — internal domain model
POST /validate — runs arbitrary attacker input through engine
POST /pipeline/score — scoring pipeline, unbounded input
POST /execute — generic rule executor
POST /edr/validate — EDR validation, unbounded input
POST /edr/validate/batch — batch — potential DoS: 5000 entity cap
POST /ingest — accepts inline datasource config
GET /ingest/datasources — exposes datasource config filenames on disk
GET /ingest/schema/{datasource_name} — connects to external DBs on attacker request
GET /rules/files — lists all rule file paths on disk
GET /rules/files/{path} — reads any rule file from disk
GET /rules/{rule_id} — reads individual rule by ID
POST /rules — creates new rule record
PUT /rules/{rule_id} — overwrites rule record
Apply dependencies=[Depends(require_api_key)] to every route listed above. The require_api_key dependency already exists in nexus/api/app.py and is used by PFP, admin, and profile-write endpoints. Add 18 regression tests asserting HTTP 401 on unauthenticated requests.
Affected files: nexus/auth.py, admin/admin.db
Current require_api_key compares the X-API-Key header against a single NEXUS_API_KEY environment variable. Any valid key can call any endpoint. No per-key scoping, no expiry, no revocation record, no client identity.
_tbl_api_keys table (see Data Model for DDL)scopes column: read, compute, adminrequire_api_key to hash-compare against DB, check scopes, check expires_atPOST /execute and POST /ingest on admin scopeAffected files: nexus/api/routes_rule_editor.py
Historical gap — fixed in HEAD c0072f4. GET /rules/files/{path} previously allowed ../ sequences to read arbitrary filesystem paths outside the rules directory.
_resolve_path() function validates that the resolved path starts with RULES_DIR.resolve(). Path traversal sequences raise HTTPException(403). Verified in test suite.
Affected files: nexus/rules/**/*.json, admin/admin.db, seeding scripts
Rules live in two locations simultaneously: JSON files on disk (seeding source) and _tbl_calcrule_rules rows in the DB (runtime query target). If JSON files are modified after seeding, the DB is stale. If DB is updated via admin panel, JSON files are stale. The TCO profile was contaminated with 65 freelance/EDR rules because a seeding script globbed without domain filtering — and this can happen again.
// IMPORT FORMAT ONLY — runtime source is admin.dbglob("*.json") without domain filterlast_seeded_from and last_seeded_at columns to _tbl_calcrule_profiles_ensure_schema() ad-hoc ALTER TABLE calls with a numbered migration chainAffected files: nexus/api/routes_rule_editor.py
PATCH, DELETE, and POST operations on rules leave no record of what changed, when, or which key changed it. No rollback is possible. In a fire-safety clearance domain (Pyrometrix), a contaminated or silently-mutated rule set is a regulatory risk.
CREATE TABLE IF NOT EXISTS _tbl_calcrule_rule_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_id TEXT NOT NULL,
changed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
changed_by TEXT, -- API key identifier (hashed key prefix)
operation TEXT NOT NULL CHECK(operation IN ('INSERT','UPDATE','DELETE')),
field TEXT, -- field name changed (NULL for INSERT/DELETE)
old_value TEXT, -- JSON-serialised previous value
new_value TEXT -- JSON-serialised new value
);
Write one row per changed field in every PATCH handler before applying the update. Expose GET /rules/{id}/history to surface rule lineage.
Affected files: scripts/deploy/, nexus/api/lambda_handler.py
EC2 deployment plan is documented in docs/aws-hosting-estimate.html but is blocked on two outstanding Pyrometrix UAT questions: (1) SZA scope alignment, (2) multi-element geometry handling. The Lambda zip does not bundle ui/index.html — the StaticFiles mount is conditional on _UI_DIR.exists() which evaluates false in a Lambda environment.
ui/ into deployment zip, or serve from S3 + CloudFront with CORS headerstests/e2e/test_smoke.py: hit /health and /ui/index.html, fail CI if non-200docker-compose.prod.yml with all secrets injected as environment variables — no .env on EC2print() statements throughout the engine and API. No correlation IDs, no log levels, no JSON format, no CloudWatch shipping. Debugging production failures requires log scraping.
print() with logging.getLogger(__name__)X-Request-ID UUID per request, injected into log contextLOG_FORMAT=json in production for structured CloudWatch ingestion/nexus/api/{env}No throttle on compute endpoints. A single caller can saturate the engine with repeated POST /pfp/clearance or POST /edr/validate/batch requests. The 5 000 entity cap on batch EDR helps but does not prevent sustained bombardment.
Add slowapi ASGI rate limiter:
/execute, /pfp/clearance, /edr/validate): 100 req / min per API key/ingest): 10 req / min per API key/rules, /profiles): 500 req / min per API keyadmin/admin.db is the only copy of all profile and rule data. EC2 instance termination results in total data loss. No WAL replication, no backup, no replica.
aws s3 cp admin/admin.db s3://nexus-db-backup/$(date +%Y-%m-%d).dbprofile_store.py uses standard SQL — migration is mechanical461 unit tests cover rule logic and structural correctness. Nothing covers concurrent request handling, DoS resilience, auth bypass attempts, or malformed formula payload injection.
locust against /pfp/clearance, 50 concurrent users, 5 min run, target P95 < 200msbandit -r nexus/ -ll, zero HIGH findings to passhypothesis or atheris against POST /execute formula field_resolve_path() with known payloads