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
2 changes: 2 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ jobs:
with: {python-version: "3.13", cache: pip, cache-dependency-path: requirements/dev.txt}
- run: pip install --require-hashes -r requirements/dev.txt
- run: python scripts/audit_dependencies.py
# The third-party inventory and notices match a regeneration in the image's base (REVIEW_1_33_PLAN.md K1).
- run: scripts/third_party.sh --check

lock-current:
# requirements/*.txt must match pyproject.toml: a dependency added without re-locking fails.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
image: ghcr.io/chaffed/openclear-ci@sha256:921ef8081264154aa92f27fd30013a2aa1f447ec1636528c4ad77a11ddb37e83 # py3.13
services:
postgres:
image: postgres:16
image: postgres:16@sha256:1beaa3084eee2676f20a9cd9928f0bf2c5bf674f8e49f81017912183d377c61c # 16, pinned (REVIEW_1_33_PLAN.md K9)
env:
POSTGRES_USER: openclear
POSTGRES_PASSWORD: openclear
Expand Down
81 changes: 81 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,86 @@
# Changelog

## Unreleased

- **A check paid by a decision is paid.** Until now only a check that matched its issued item
cleanly was recorded as paid; one paid by a decision (an accepted amount mismatch, or every
check on a reverse positive pay account, paid at the cut-off by silence) stayed pending, so the
same check presented again was the same item again, and on a reverse account was paid again.
From the cut-off, a paid check is paid, its issued item with it, and a returned check returned,
so a second presentment is a duplicate wherever it comes from; the upgrade brings every
decision already final into line. *Pay all N to review* now pays exactly the checks the page
listed, not any N. A teller platform that lost the answer to `POST /api/v1/checks/cashed` is
told, on its retry, which paid item the first call recorded and when
(`plans/REVIEW_1_33_PLAN.md` RV1).
- **The teller answer gives nothing the caller didn't send.** A decline for a payee the teller
didn't send no longer carries the payee it's asking for, and `issued` names the payee only
when the one sent differs. A check another teller verified in the last 15 minutes and hasn't
recorded cashed is declined `being_cashed`, so two branches can't both pay it out. Ten declines
of any kind within an hour (not only "not issued": a wrong amount reveals the issued amount)
raise `teller.verify.probing`, counted by teller, branch, API client, and now by the person
when a person's token asks. A check on an account whose customer's service has ended answers
`not_on_positive_pay`, as the guide said, instead of failing (RV2).
- **A check that arrives late gets a look before a default decides it.** Its decision deadline
is never later than the cut-off on its return day; when a file reaches OpenClear after that
cut-off (a core posting yesterday's items mid-morning), the item was due at once and the
customer's default decision applied at the next sweep, unseen. It now gets the bank's minimum
review time, never past the midnight deadline. A bank that opens on the Federal Reserve's
holidays now has its cut-off on them too, as the setting said. The decisions CSV's columns are
listed in the BI guide, with the `after_return_deadline` column 1.29.0 added at the end (RV3).
- **Deleting a bank's data clears every storage area.** The deletion cleared five of the seven:
the decision files its core took, and fraud-scoring artifacts, stayed. It now clears every
area from storage's own list, the certificate names them all, and a test walks the list. The
bank is frozen as the deletion job starts (sign-ins and tokens refused, its queued jobs gone),
so nothing is added while the rows go. A deletion the queue gives up on is marked failed, shown
on the bank's offboarding page and raised as a critical event, instead of showing as running
at a time long past. Offboarding a bank, deleting its data and ending a customer's positive pay
always need a second person: with nobody else able to approve, the request is refused rather
than applied after the day's cooling-off (RV4).
- **A customer's positive pay ends at midnight on its last day, everywhere.** Sign-in, files
naming its accounts and teller answers now read the last day itself, not the daily run that
follows it (which could be a morning later), and the end is recorded as the end of that day, so
the 90-day clock and every page agree with the day the customer was told. A core's customer
sync can no longer bring an ended customer back: `PUT /api/v1/customers/{number}` with
`is_active: true` is refused with 409, and restoring is the two-administrator change on the
customer's page. Ending service signs its people out of this bank only. When a customer's data
is deleted, the bank's attestations and access reviews about it stay, no longer naming it; a
person kept because the action log names them keeps nothing personal (no email, phone,
password or sessions). Dates on the customer page, the banner and the Deleted customers list are
the bank's days (RV5).
- **Custom fields: numbers, limits, files and messages.** A negative number in a `cf_` column of
the decisions CSV reached the core as `'-5`, the spreadsheet-formula guard's doing; a plain
signed number now passes as one. A customer's reader asking the decision feed for a field not
shown to the customer is told there's no such field, as for one that doesn't exist. Restoring an
archived field counts against the 25-field and 3-in-list limits. An update file naming a field
the bank doesn't have, or has archived, is refused whole instead of failing every row; a text
cell reading `NA`, `null` or `None` in such a file is that text, not blank. A whole number takes
only the digits 0–9, a date only `YYYY-MM-DD`, a decimal no `_`, as their messages said; a form
reports every field's problem at once; a dropped choice option is retired only while a record
holds it. Field types read as words ("Whole number", "Yes/no"). **Rate limiting with Redis:**
while Redis can't be reached each instance now limits on its own instead of allowing
everything, the outage is a `rate_limit.backend_down` security event (once per outage), and the
Controls page says so (new control OC-SC-08) (RV6).
- **Incident contacts and notices.** A bank may name only its CEO and CIO, the fallback 12 CFR
304.24 gives, and notices then go to them (the page and the control said so; the code required a
named contact). The form shows a row for each contact and one more, up to ten. The yearly
confirmation is a security event (`incident_contact.confirmed`). The platform's Sent table says
which banks each notice reached and how many addresses, and marks a bank with nobody to email;
"Offboarding" is no longer a kind an operator can pick. A check refused at a branch is described
in words on the customer's Exceptions page ("issued for another amount"). The SAML signing-key
page says that finishing before the identity provider has the new certificate stops sign-in and
can't be undone, and that withdrawing after it switched needs the old certificate back (RV7).
- **Refusals are sentences written for people.** Every page that shows why something was refused
now shows the sentence OpenClear wrote for it; an exception from a library or a database driver
shows a fixed sentence instead and is logged (the class of finding CodeQL #176 raised, closed
everywhere rather than in one place). Replacing a customer's SAML signing key from the bank's
side goes through the customer's own second-person rule. **For operators:** `scripts/third_party.sh
--check` regenerates the third-party inventory and notices and fails when the committed copies
differ; the Security workflow runs it. The tests' Postgres image is pinned by digest. A
posted-check file with an absurdly long number in an amount or a trailer is refused as a row or
a trailer problem, not an error (RV8).

- **Dependencies:** pyasn1 0.6.5 (CVE-2026-119197, CVE-2026-119195) in the image and for development.

## 1.33.0 — 2026-10-30

- **Third-party components** (`plans/THIRD_PARTY_PLAN.md`). A page listing everything OpenClear
Expand Down
2 changes: 1 addition & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -8919,7 +8919,7 @@ Library.

`LICENSE.txt`: the same text as psycopg's, above.

### pyasn1 0.6.4 (BSD-2-Clause)
### pyasn1 0.6.5 (BSD-2-Clause)

`LICENSE.rst`:

Expand Down
2 changes: 1 addition & 1 deletion docs/compliance/asvs.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Requirements not fully met are recorded with their reason, here and in [OWASP_PL
| **V2.3.1** Verify that the application will only process business logic flows for the same user in the expected sequential step order and without skipping steps. | 1 | Met | `src/openclear/services/change_request_service.py`, OC-AC-05, OC-AC-06 | |
| **V2.3.2** Verify that business logic limits are implemented per the application's documentation to avoid business logic flaws being exploited. | 2 | Met | `src/openclear/services/change_request_service.py`, OC-AC-05, OC-AC-06 | |
| **V2.3.3** Verify that transactions are being used at the business logic level such that either a business logic operation succeeds in its entirety or it is rolled back… | 2 | Met | `src/openclear/services/change_request_service.py`, OC-AC-05, OC-AC-06 | |
| **V2.3.4** Verify that business logic level locking mechanisms are used to ensure that limited quantity resources (such as theater seats or delivery slots) cannot be do… | 2 | Met | `src/openclear/repositories/exception_repo.py`, `src/openclear/services/decision_service.py`, `src/openclear/services/decision_outbox_service.py`, `test_two_decisions_at_once_one_wins_and_the_other_is_told`, `test_a_check_presented_twice_at_once_is_paid_once`, `test_two_outbox_runs_at_once_write_one_file` | A row lock per item for every decision change, a check paid once however many times it's presented, and one writer per bank for decision files (FULL_REVIEW_PLAN.md FR1-FR3), each proved by two sessions at once on Postgres. |
| **V2.3.4** Verify that business logic level locking mechanisms are used to ensure that limited quantity resources (such as theater seats or delivery slots) cannot be do… | 2 | Met | `src/openclear/repositories/exception_repo.py`, `src/openclear/services/decision_service.py`, `src/openclear/services/decision_outbox_service.py`, `test_two_decisions_at_once_one_wins_and_the_other_is_told`, `test_a_check_presented_twice_at_once_is_paid_once`, `test_two_outbox_runs_at_once_write_one_file`, `test_a_check_cashed_while_its_file_presentment_is_in_flight_is_paid_once`, `test_one_verification_cashed_twice_at_once_records_one_payment` | A row lock per item for every decision change, a check paid once however many times it's presented, and one writer per bank for decision files (FULL_REVIEW_PLAN.md FR1-FR3), each proved by two sessions at once on Postgres. |
| **V2.4.1** Verify that anti-automation controls are in place to protect against excessive calls to application functions that could lead to data exfiltration, garbage-d… | 2 | Met | `src/openclear/web/rate_limit.py` | |

## V3 Web Frontend Security
Expand Down
1 change: 1 addition & 0 deletions docs/compliance/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Inventory version 1.3. The same list, with each check's live status for your ban
|---|---|---|---|---|---|---|---|
| OC-SC-01 | **Secrets encrypted in the database.** Secrets kept in the database (SSO keys, delivery credentials, plugin settings) are encrypted with AES-256-GCM under versioned keys, and re-encrypted when a key is rotated. | **SC-28(1)** Cryptographic Protection<br>**SC-12** Cryptographic Key Establishment and Management<br>**SC-13** Cryptographic Protection | PR.DS-01, PR.DS-02, PR.DS-10 | preventive, built in | `field_encryption` (live)<br>`key_age` (live) | H-SC-12-01 | auth/crypto.py |
| OC-SC-02 | **Encryption at rest stated.** The host's statement of how disks and the database are encrypted is shown on the Controls page, which warns until there is one. | **SC-28** Protection of Information at Rest | PR.DS-01 | detective, built in | `storage_encryption` (live) | H-SC-28(1)-01 | Controls → Storage encryption |
| OC-SC-08 | **Sign-in and API attempts rate-limited.** Sign-in, token and password-reset attempts are limited per address and per route; with several instances the counts are shared through Redis, and while Redis can't be reached each instance limits on its own and the Controls page says so. | **SC-5** Denial-of-service Protection<br>**AC-7** Unsuccessful Logon Attempts | PR.AA-03, PR.IR-01, DE.CM-01 | preventive, built in | `rate_limit_backend` (live) | — | web/rate_limit.py; Controls → Rate limits shared across instances |
| OC-SC-03 | **Files signed.** Every uploaded file is kept with its SHA-256 and a signature, and every webhook and evidence bundle OpenClear sends is signed. | **SI-7(6)** Cryptographic Protection<br>**SC-8** Transmission Confidentiality and Integrity | ID.RA-09, PR.DS-01, PR.DS-02, PR.DS-10, PR.PS-02, DE.CM-09 | preventive, built in | — | — | An upload's page; webhooks |
| OC-SC-04 | **Browser protections.** Cookies are HTTP-only, and secure over HTTPS, which is marked with HSTS; every form carries a CSRF token; and a content security policy limits scripts to OpenClear's own. | **SC-23** Session Authenticity<br>**SC-8** Transmission Confidentiality and Integrity<br>**SC-18** Mobile Code | PR.DS-02 | preventive, built in | `transport_security` (live) | H-SC-8(1)-01 | web/security.py |
| OC-SC-05 | **Edge trusted only as configured.** The caller's address is taken only from the configured edge's header, an edge secret can be required, and the Controls page shows the edge in front. | **SC-7** Boundary Protection | PR.DS-01, PR.DS-02, PR.DS-10, PR.IR-01, DE.CM-01 | preventive, configurable | `edge` (live) | H-SC-7-01 | Controls → Edge in front; docs/guides/edge.md |
Expand Down
6 changes: 3 additions & 3 deletions docs/compliance/csf.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ NIST's mapping links no subcategory to `MP-6`, `AU-10`, `MA-4`, `SC-18`, `SC-23`
|---|---|---|
| **PR.AA-01** Identities and credentials for authorized users, services, and hardware are managed by the organization | OC-AC-02, OC-IA-01, OC-IA-02, OC-IA-03, OC-IA-04, OC-IA-05, OC-SI-13 | B-AC-2-01, B-AC-2(1)-01, B-IA-2(1)-01, B-IA-2(2)-01, B-IA-2(2)-02, B-IA-2(2)-03, B-IA-5(1)-01, B-IA-5-01, B-AC-2-02, B-IA-8-01, C-AC-2-01, C-AC-2-02, C-AC-2(1)-01, C-IA-2-01, C-IA-5-01 |
| **PR.AA-02** Identities are proofed and bound to credentials based on the context of interactions | — | B-IA-12-01 |
| **PR.AA-03** Users, services, and hardware are authenticated | OC-AC-10, OC-IA-01, OC-IA-02, OC-IA-03, OC-IA-04, OC-IA-05, OC-SC-06 | B-IA-2(1)-01, B-IA-2(2)-01, B-IA-2(2)-02, B-IA-2(2)-03, B-IA-5(1)-01, B-IA-5-01, B-IA-8-01, C-IA-2-01, C-IA-5-01 |
| **PR.AA-03** Users, services, and hardware are authenticated | OC-AC-10, OC-IA-01, OC-IA-02, OC-IA-03, OC-IA-04, OC-IA-05, OC-SC-08, OC-SC-06 | B-IA-2(1)-01, B-IA-2(2)-01, B-IA-2(2)-02, B-IA-2(2)-03, B-IA-5(1)-01, B-IA-5-01, B-IA-8-01, C-IA-2-01, C-IA-5-01 |
| **PR.AA-04** Identity assertions are protected, conveyed, and verified | — | — |
| **PR.AA-05** Access permissions, entitlements, and authorizations are defined in a policy, managed, enforced, and reviewed, and incorporate the principles of least privilege and separation of duties | OC-AC-01, OC-AC-02, OC-AC-03, OC-AC-04, OC-AC-05, OC-AC-06, OC-AC-07, OC-AC-08, OC-AC-09, OC-AC-12, OC-AC-13, OC-IA-03, OC-SI-08, OC-SI-09, OC-SI-13 | B-AC-2-01, B-AC-2(1)-01, B-AC-6(7)-01, B-AC-6-01, B-AC-5-01, B-AC-3(2)-01, B-AC-2-02, C-AC-2-01, C-AC-2-02, C-AC-2(1)-01, C-AC-6(7)-01, C-AC-5-01, C-AC-3-01, H-AC-6(5)-01 |
| **PR.AA-06** Physical access to assets is managed, monitored, and enforced commensurate with risk | — | — |
Expand All @@ -98,7 +98,7 @@ NIST's mapping links no subcategory to `MP-6`, `AU-10`, `MA-4`, `SC-18`, `SC-23`
| **PR.PS-04** Log records are generated and made available for continuous monitoring | OC-AU-01, OC-AU-03, OC-AU-04, OC-AU-05, OC-SI-10 | B-AU-6-01, B-AU-11-01, B-AU-11-02 |
| **PR.PS-05** Installation and execution of unauthorized software are prevented | — | — |
| **PR.PS-06** Secure software development practices are integrated, and their performance is monitored throughout the software development life cycle | — | B-SA-11-01 |
| **PR.IR-01** Networks and environments are protected from unauthorized logical access and usage | OC-AC-01, OC-AC-03, OC-AC-04, OC-AC-06, OC-AC-07, OC-AC-08, OC-AC-09, OC-AC-12, OC-AC-13, OC-SC-05, OC-SC-06, OC-SC-07, OC-SI-08 | B-AC-3(2)-01, C-AC-3-01, H-SC-7-01 |
| **PR.IR-01** Networks and environments are protected from unauthorized logical access and usage | OC-AC-01, OC-AC-03, OC-AC-04, OC-AC-06, OC-AC-07, OC-AC-08, OC-AC-09, OC-AC-12, OC-AC-13, OC-SC-08, OC-SC-05, OC-SC-06, OC-SC-07, OC-SI-08 | B-AC-3(2)-01, C-AC-3-01, H-SC-7-01 |
| **PR.IR-02** The organization's technology assets are protected from environmental threats | OC-SI-05, OC-CP-01 | B-CP-2-02, B-CP-2-01, B-CP-2-03, H-CP-2-01 |
| **PR.IR-03** Mechanisms are implemented to achieve resilience requirements in normal and adverse situations | OC-SI-05, OC-SI-07, OC-CM-02, OC-CP-01, OC-IR-01, OC-IR-02 | B-IR-6-03, B-IR-6-04, B-IR-6-05, B-IR-6-06, B-IR-6-07, B-IR-6-08, B-IR-6-02, B-CP-2-02, B-CP-2-01, B-CP-2-03, B-IR-6-01, C-IR-6-01, H-CP-9-01, H-CP-2-01, H-IR-6-01, H-IR-6-02, H-IR-8-01 |
| **PR.IR-04** Adequate resource capacity to ensure availability is maintained | — | — |
Expand All @@ -107,7 +107,7 @@ NIST's mapping links no subcategory to `MP-6`, `AU-10`, `MA-4`, `SC-18`, `SC-23`

| Subcategory | OpenClear controls | CUECs |
|---|---|---|
| **DE.CM-01** Networks and network services are monitored to find potentially adverse events | OC-AC-02, OC-AC-07, OC-AC-13, OC-IA-03, OC-AU-01, OC-AU-04, OC-SC-05, OC-SC-06, OC-SC-07, OC-SI-03, OC-SI-05, OC-SI-09, OC-SI-10, OC-SI-11, OC-SI-13, OC-CM-02, OC-CA-01, OC-CA-02, OC-CA-03 | B-AC-2-01, B-AC-2(1)-01, B-CM-3-01, B-AC-2-02, B-CA-7-01, B-CA-7-02, B-SI-4-01, B-SI-4-02, B-SI-4-03, C-AC-2-01, C-AC-2-02, C-AC-2(1)-01, C-SI-4-02, C-SI-4-01, H-SC-7-01, H-SI-4-01 |
| **DE.CM-01** Networks and network services are monitored to find potentially adverse events | OC-AC-02, OC-AC-07, OC-AC-13, OC-IA-03, OC-AU-01, OC-AU-04, OC-SC-08, OC-SC-05, OC-SC-06, OC-SC-07, OC-SI-03, OC-SI-05, OC-SI-09, OC-SI-10, OC-SI-11, OC-SI-13, OC-CM-02, OC-CA-01, OC-CA-02, OC-CA-03 | B-AC-2-01, B-AC-2(1)-01, B-CM-3-01, B-AC-2-02, B-CA-7-01, B-CA-7-02, B-SI-4-01, B-SI-4-02, B-SI-4-03, C-AC-2-01, C-AC-2-02, C-AC-2(1)-01, C-SI-4-02, C-SI-4-01, H-SC-7-01, H-SI-4-01 |
| **DE.CM-02** The physical environment is monitored to find potentially adverse events | OC-CA-01, OC-CA-02, OC-CA-03 | B-CA-7-01, B-CA-7-02 |
| **DE.CM-03** Personnel activity and technology usage are monitored to find potentially adverse events | OC-AC-02, OC-IA-03, OC-AU-01, OC-AU-04, OC-SI-13, OC-CA-01, OC-CA-02, OC-CA-03 | B-AC-2-01, B-AC-2(1)-01, B-AC-2-02, B-CA-7-01, B-CA-7-02, C-AC-2-01, C-AC-2-02, C-AC-2(1)-01 |
| **DE.CM-06** External service provider activities and services are monitored to find potentially adverse events | OC-AU-04, OC-SI-03, OC-SI-05, OC-SI-09, OC-SI-10, OC-SI-11, OC-CA-01, OC-CA-02, OC-CA-03 | B-CA-7-01, B-CA-7-02, B-SI-4-01, B-SI-4-02, B-SI-4-03, B-SA-4-01, B-SA-9-02, B-SA-9-03, B-SA-9-01, C-SI-4-02, C-SI-4-01, H-SI-4-01 |
Expand Down
2 changes: 1 addition & 1 deletion docs/compliance/third-party.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Installed from the hashed lock `requirements/runtime.txt` and nothing else.
| propcache | 0.5.4 | Apache-2.0 | for aiohttp, yarl |
| psycopg | 3.3.6 | LGPL-3.0-only | OpenClear |
| psycopg-binary | 3.3.6 | LGPL-3.0-only | for psycopg |
| pyasn1 | 0.6.4 | BSD-2-Clause | for pyasn1-modules, webauthn |
| pyasn1 | 0.6.5 | BSD-2-Clause | for pyasn1-modules, webauthn |
| pyasn1-modules | 0.4.2 | BSD | for webauthn |
| pycparser | 3.12 | BSD-3-Clause | for cffi |
| pydantic | 2.14.1 | MIT | OpenClear |
Expand Down
Loading
Loading