Cratis security review
Event sourcing changes the shape of a security review. A mistake in an ordinary
application is a bug you fix; a secret written into an append is a fact that
lives in the log forever and cannot be edited out. The event-sourcing section
below is the one that is specific to this stack, and it is the one worth reading
first.
Verified product sources
| Package |
Version |
Purpose |
Cratis.Arc.Chronicle |
22.10.4 |
[NotAudited] in Cratis.Arc.Chronicle.Commands; the ARCCHR0009 analyzer |
Cratis.Chronicle |
16.45.2 |
[PII], [Subject], redaction, namespace isolation |
Cratis.Arc.Core |
22.10.4 |
Authorization filters, CommandResult.Unauthorized |
Reverify against the owning product repository before asserting a framework
guarantee this file does not already state.
Route near misses
- General correctness and maintainability: use
cratis-code-review.
- Scalability and resource use: use
cratis-performance-review.
- Implementing authentication, authorization or identity: use
cratis-arc-authentication-authorization-and-identity. This skill reviews;
it does not build.
- Compliance mechanics —
[PII], subject resolution, erasure, redaction: use
cratis-chronicle-compliance.
Step 1 — Event sourcing: the permanent-record checks
- Every
[Command] property holding a secret is marked [NotAudited]. A
command's property values are written to the causation of every event it
appends, and causation is as permanent as the events. Prefer the marking on
the concept type so it travels everywhere the value appears.
ARCCHR0009 catches properties whose names read as secrets — so read the
properties whose names do not say what they hold, because the analyzer cannot.
- Personal data is
[PII], not [NotAudited]. They are different
mechanisms with different consequences: [NotAudited] withholds a value from
the causation chain; [PII] enrolls it in per-subject encryption and erasure.
A password is [NotAudited]. An email address is [PII]. Neither substitutes
for the other.
- No secret, token, API key or password in an event property or a read model.
- Event-source ids are generated server-side, never accepted from an untrusted
client where the id grants access to a stream.
- Upcasting and event-type migration logic cannot introduce a property the
original contract did not carry.
- Uniqueness cannot be bypassed by concurrent writes — it is enforced by a
Chronicle constraint, not by a read-model pre-check.
- Cross-tenant writes cannot bypass a constraint that is scoped per namespace.
Step 2 — Input validation and injection
- Every command property is validated before use — null, empty, range, format.
- No raw SQL concatenation; parameterized queries or EF Core only.
- No user-supplied value reaches
Path.Combine, a File.* call, a shell
command, or process arguments.
- No user-supplied value becomes an event-store key without sanitization.
Step 3 — Authentication and authorization
- Every exposed endpoint is either authorized or explicitly anonymous with a
stated reason.
- Authorization is expressed at the boundary — an attribute, a policy, a command
filter — never as an
if on roles inside Handle().
- Tenant isolation holds: no cross-namespace data is reachable without
authorization.
- Claims are verified before acting on identity-dependent command data. A client
must not be able to assert who it is through a command property.
- Note that an unauthorized command result maps to HTTP 403, not 401 — a
reviewer reading logs for 401s will miss authorization failures.
Step 4 — Data exposure
- No personal data is returned to a caller that did not supply it.
- Query results are scoped to the requesting tenant and user. A query that can
return all-tenant data is a finding even when no current caller reaches it.
- Response payloads carry only fields the client uses. Over-fetching is an
exposure surface, not only a performance one.
- A managed read-model document holds one subject's personal data. Mixing
several people's data in one document breaks erasure.
Step 5 — Secrets and configuration
- No secret in source, in a configuration file, or in a specification fixture.
- Secrets come from environment variables or a secrets manager.
- No hard-coded connection string outside test code.
Step 6 — Frontend
- No user-supplied value in
dangerouslySetInnerHTML.
- No token or secret in
localStorage — use an httpOnly cookie or in-memory
state.
- Command payloads carry only the minimum required fields.
- No client-side access control that is not also enforced server-side. A
disabled button is a usability affordance, never a control.
Step 7 — Report
Open with one line:
Security review: No issues / Low-risk findings / Blocking issues found
Group findings by the section that produced them, and classify each:
| Risk |
Meaning |
| Critical |
Must be fixed before merge |
| Medium |
Should be fixed soon; state what makes it not-critical |
| Low |
Fix when convenient |
Close with a per-section summary table, and name what you did not review —
the paths, the surfaces, and the axes out of scope. A security report listing
only findings reads as a clean bill of health for everything it never opened.
What breaks
- A finding is a policy preference in framework clothing. Which roles exist,
which data is sensitive, and which retention applies are the product's calls,
not the framework's. State them as policy questions for the owner, not as
contracts.
[NotAudited] is used for personal data. The value stays out of causation
but is never encrypted and never enrolled in erasure — the opposite of what a
subject-rights request needs.
- The review assumes the analyzer covered the secrets.
ARCCHR0009 matches
names. A property called Value holding an API key passes it silently.
- A missing check is reported as "verified". Unknown is not pass. If a
surface could not be reached, say so as
indeterminate rather than omitting
it.
How it is proven
The build and specifications are green before the report is written; each
finding cites the file and line; and the report states explicitly which
surfaces were and were not examined.
1---2name: cratis-security-review3description: Perform a focused security review of changed code in a Cratis application — injection, authentication and authorization, data exposure, secrets, event-sourcing-specific exposure, and the frontend — and report findings by risk. Use when asked for a security review or audit. Do not use to implement authentication and do not report a policy preference as a framework contract.4license: MIT5---67# Cratis security review89Event sourcing changes the shape of a security review. A mistake in an ordinary10application is a bug you fix; a secret written into an append is a fact that11lives in the log forever and cannot be edited out. The event-sourcing section12below is the one that is specific to this stack, and it is the one worth reading13first.1415## Verified product sources1617| Package | Version | Purpose |18| --- | --- | --- |19| `Cratis.Arc.Chronicle` | `22.10.4` | `[NotAudited]` in `Cratis.Arc.Chronicle.Commands`; the `ARCCHR0009` analyzer |20| `Cratis.Chronicle` | `16.45.2` | `[PII]`, `[Subject]`, redaction, namespace isolation |21| `Cratis.Arc.Core` | `22.10.4` | Authorization filters, `CommandResult.Unauthorized` |2223Reverify against the owning product repository before asserting a framework24guarantee this file does not already state.2526## Route near misses2728- General correctness and maintainability: use `cratis-code-review`.29- Scalability and resource use: use `cratis-performance-review`.30- Implementing authentication, authorization or identity: use31 `cratis-arc-authentication-authorization-and-identity`. This skill reviews;32 it does not build.33- Compliance mechanics — `[PII]`, subject resolution, erasure, redaction: use34 `cratis-chronicle-compliance`.3536## Step 1 — Event sourcing: the permanent-record checks3738- **Every `[Command]` property holding a secret is marked `[NotAudited]`.** A39 command's property values are written to the causation of every event it40 appends, and causation is as permanent as the events. Prefer the marking on41 the concept type so it travels everywhere the value appears.42 `ARCCHR0009` catches properties whose *names* read as secrets — so read the43 properties whose names do not say what they hold, because the analyzer cannot.44- **Personal data is `[PII]`, not `[NotAudited]`.** They are different45 mechanisms with different consequences: `[NotAudited]` withholds a value from46 the causation chain; `[PII]` enrolls it in per-subject encryption and erasure.47 A password is `[NotAudited]`. An email address is `[PII]`. Neither substitutes48 for the other.49- No secret, token, API key or password in an event property or a read model.50- Event-source ids are generated server-side, never accepted from an untrusted51 client where the id grants access to a stream.52- Upcasting and event-type migration logic cannot introduce a property the53 original contract did not carry.54- Uniqueness cannot be bypassed by concurrent writes — it is enforced by a55 Chronicle constraint, not by a read-model pre-check.56- Cross-tenant writes cannot bypass a constraint that is scoped per namespace.5758## Step 2 — Input validation and injection5960- Every command property is validated before use — null, empty, range, format.61- No raw SQL concatenation; parameterized queries or EF Core only.62- No user-supplied value reaches `Path.Combine`, a `File.*` call, a shell63 command, or process arguments.64- No user-supplied value becomes an event-store key without sanitization.6566## Step 3 — Authentication and authorization6768- Every exposed endpoint is either authorized or explicitly anonymous with a69 stated reason.70- Authorization is expressed at the boundary — an attribute, a policy, a command71 filter — never as an `if` on roles inside `Handle()`.72- Tenant isolation holds: no cross-namespace data is reachable without73 authorization.74- Claims are verified before acting on identity-dependent command data. A client75 must not be able to assert who it is through a command property.76- Note that an unauthorized command result maps to HTTP **403**, not 401 — a77 reviewer reading logs for 401s will miss authorization failures.7879## Step 4 — Data exposure8081- No personal data is returned to a caller that did not supply it.82- Query results are scoped to the requesting tenant and user. A query that can83 return all-tenant data is a finding even when no current caller reaches it.84- Response payloads carry only fields the client uses. Over-fetching is an85 exposure surface, not only a performance one.86- A managed read-model document holds one subject's personal data. Mixing87 several people's data in one document breaks erasure.8889## Step 5 — Secrets and configuration9091- No secret in source, in a configuration file, or in a specification fixture.92- Secrets come from environment variables or a secrets manager.93- No hard-coded connection string outside test code.9495## Step 6 — Frontend9697- No user-supplied value in `dangerouslySetInnerHTML`.98- No token or secret in `localStorage` — use an `httpOnly` cookie or in-memory99 state.100- Command payloads carry only the minimum required fields.101- No client-side access control that is not also enforced server-side. A102 disabled button is a usability affordance, never a control.103104## Step 7 — Report105106Open with one line:107108> **Security review: No issues / Low-risk findings / Blocking issues found**109110Group findings by the section that produced them, and classify each:111112| Risk | Meaning |113| --- | --- |114| **Critical** | Must be fixed before merge |115| **Medium** | Should be fixed soon; state what makes it not-critical |116| **Low** | Fix when convenient |117118Close with a per-section summary table, and **name what you did not review** —119the paths, the surfaces, and the axes out of scope. A security report listing120only findings reads as a clean bill of health for everything it never opened.121122## What breaks123124- **A finding is a policy preference in framework clothing.** Which roles exist,125 which data is sensitive, and which retention applies are the product's calls,126 not the framework's. State them as policy questions for the owner, not as127 contracts.128- **`[NotAudited]` is used for personal data.** The value stays out of causation129 but is never encrypted and never enrolled in erasure — the opposite of what a130 subject-rights request needs.131- **The review assumes the analyzer covered the secrets.** `ARCCHR0009` matches132 names. A property called `Value` holding an API key passes it silently.133- **A missing check is reported as "verified".** Unknown is not pass. If a134 surface could not be reached, say so as `indeterminate` rather than omitting135 it.136137## How it is proven138139The build and specifications are green before the report is written; each140finding cites the file and line; and the report states explicitly which141surfaces were and were not examined.