mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59:26 +00:00
Implemented with Codex. Cluster deploy pending the k3s kubeconfig and the scoped account. Co-Authored-By: Claude Opus 5.5 <[email protected]>
256 lines
12 KiB
Markdown
256 lines
12 KiB
Markdown
# RustFS setup for the memory store
|
|
|
|
The plane's memory store can live in an S3-compatible bucket. RustFS is an S3-compatible object
|
|
storage server.
|
|
|
|
- `OUSAST_MEMORY=s3://sast-memory` selects `S3Store` (`src/openultrasast/plane/memory.py`).
|
|
- `S3Store` talks to the server through boto3 (the AWS SDK for Python).
|
|
- Any S3-compatible store works if it has bucket versioning, lifecycle rules, object tags and S3 Select
|
|
(a server-side query over JSON objects).
|
|
- RustFS is the server the contract tests run against.
|
|
- The maintainer's server is RustFS at `files.because-security.com`. The bucket is `sast-memory`. The
|
|
store sits at the bucket root.
|
|
|
|
[Memory](memory.md) describes what the store keeps there.
|
|
|
|
**The store never configures its bucket.** An admin sets the bucket up once with admin credentials.
|
|
The store runs as an agent account. That account can read the bucket's settings but not change them.
|
|
It reads and writes objects freely. Every time the store opens, it checks the setup. It refuses to start
|
|
if anything is missing.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
admin["Admin, once: versioning on, runs/ expiry rule"] --> bucket[("Bucket sast-memory")]
|
|
agent["Agent account: read settings, read and write objects and tags"] --> verify{"verify_bucket at startup"}
|
|
bucket --> verify
|
|
verify -- "all present" --> store["S3Store serves reads and writes"]
|
|
verify -- "anything missing" --> refuse["MemoryStoreError naming each missing piece and its admin command"]
|
|
```
|
|
|
|
## One-time admin steps
|
|
|
|
Run these with **admin** credentials (not the agent's). Both use the AWS CLI against the RustFS
|
|
endpoint.
|
|
|
|
**1. Turn versioning on.** Provenance cites object version ids.
|
|
|
|
```bash
|
|
aws s3api put-bucket-versioning \
|
|
--endpoint-url https://files.because-security.com \
|
|
--bucket sast-memory \
|
|
--versioning-configuration Status=Enabled
|
|
```
|
|
|
|
**2. Expire raw run outputs under `runs/` after 30 days.** Everything else is kept: `repos/`, `facts/`,
|
|
`index.jsonl` and the blobs. So the rule must be a plain prefix filter on `runs/`.
|
|
|
|
```bash
|
|
aws s3api put-bucket-lifecycle-configuration \
|
|
--endpoint-url https://files.because-security.com \
|
|
--bucket sast-memory \
|
|
--lifecycle-configuration '{"Rules":[{"ID":"ousast-runs-expiry","Status":"Enabled","Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}}]}'
|
|
```
|
|
|
|
`put-bucket-lifecycle-configuration` **replaces every rule on the bucket**. If the bucket already
|
|
has rules, merge this one into them first.
|
|
|
|
The rule id `ousast-runs-expiry` and the 30 days are the store's defaults (`RUNS_RULE_ID`,
|
|
`RUNS_EXPIRE_DAYS`). The store accepts any enabled, untagged prefix rule on `runs/` with a number of days.
|
|
|
|
To read the result back:
|
|
|
|
```bash
|
|
aws s3api get-bucket-versioning --endpoint-url https://files.because-security.com --bucket sast-memory
|
|
aws s3api get-bucket-lifecycle-configuration --endpoint-url https://files.because-security.com --bucket sast-memory
|
|
```
|
|
|
|
## The agent policy
|
|
|
|
Attach this policy to the agent account. That is the account whose keys go into `.env`. It is the
|
|
complete policy, and nothing else is needed. It grants no `Put*` on versioning or lifecycle.
|
|
|
|
```json
|
|
{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Sid": "BucketSettingsReadOnly",
|
|
"Effect": "Allow",
|
|
"Action": [
|
|
"s3:ListBucket",
|
|
"s3:ListBucketVersions",
|
|
"s3:GetBucketLocation",
|
|
"s3:GetBucketVersioning",
|
|
"s3:GetLifecycleConfiguration"
|
|
],
|
|
"Resource": ["arn:aws:s3:::sast-memory"]
|
|
},
|
|
{
|
|
"Sid": "ObjectsReadWrite",
|
|
"Effect": "Allow",
|
|
"Action": [
|
|
"s3:GetObject",
|
|
"s3:GetObjectVersion",
|
|
"s3:PutObject",
|
|
"s3:DeleteObject",
|
|
"s3:DeleteObjectVersion",
|
|
"s3:GetObjectTagging",
|
|
"s3:PutObjectTagging",
|
|
"s3:GetObjectVersionTagging"
|
|
],
|
|
"Resource": ["arn:aws:s3:::sast-memory/*"]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
| Statement | Resource | In one line |
|
|
| --- | --- | --- |
|
|
| `BucketSettingsReadOnly` | `arn:aws:s3:::sast-memory` | The agent can list the bucket and its versions and *read* its location, versioning and lifecycle settings, which is what the startup check needs; it cannot change them. |
|
|
| `ObjectsReadWrite` | `arn:aws:s3:::sast-memory/*` | The agent reads, writes and deletes objects and their versions and reads and writes object tags; this covers row objects, blobs, the index, the Select probe, and the `kind` tag the store filters by. |
|
|
|
|
So the agent writes objects freely but cannot change bucket settings. Turning versioning off or
|
|
removing the expiry rule needs the admin.
|
|
|
|
## The `.env` settings
|
|
|
|
The store reads its endpoint and credentials from `.env` in the working directory or from the
|
|
environment (`s3_settings`). It never reads them from a manifest. It never prints them. The names are
|
|
the ones the AWS SDKs and CLI use for credentials. So one `.env` serves both the store and `aws s3api`.
|
|
|
|
| Variable | Value for this server | Notes |
|
|
| --- | --- | --- |
|
|
| `OUSAST_MEMORY` | `s3://sast-memory` | `s3://<bucket>[/<prefix>]`; `s3://` alone uses `S3_BUCKET`; unset means the local `FileStore` |
|
|
| `S3_ENDPOINT` | `https://files.because-security.com` | the full URL, scheme included (`https://host[:port]`); required |
|
|
| `AWS_ACCESS_KEY_ID` | the agent account's access key | required |
|
|
| `AWS_SECRET_ACCESS_KEY` | the agent account's secret key | required |
|
|
| `AWS_SESSION_TOKEN` | optional | for temporary credentials |
|
|
| `S3_REGION` | optional (`us-east-1` when unset) | the SigV4 signing region; spares a GetBucketLocation call |
|
|
| `S3_BUCKET` | `sast-memory` | the bucket `s3://` names when the URL has none; also the real-server tests' default bucket |
|
|
|
|
The client signs with SigV4 (AWS's request-signing scheme). It uses path-style addressing
|
|
(`https://host/bucket/key`), which RustFS needs. The SDK's own checksum trailers are off. Not every
|
|
S3-compatible server accepts them.
|
|
|
|
- **Never commit `.env`.** It is gitignored (`.gitignore`: `.env`, `.env.*`, except
|
|
`.env.example`). `.env.example` lists these names with empty values.
|
|
- **`.env` never overrides a variable already exported in your shell** (`config.load_dotenv`).
|
|
A stale `export AWS_SECRET_ACCESS_KEY=...` in your profile silently wins over the file. `unset`
|
|
it when a change in `.env` seems to have no effect.
|
|
- The store needs the `s3` extra (`uv sync --extra s3`, which installs boto3). These are the
|
|
startup errors:
|
|
- Without the extra, opening the store fails with
|
|
`OUSAST_MEMORY=s3://... needs boto3: install openultrasast[s3]`.
|
|
- A missing variable fails with `OUSAST_MEMORY=s3://... needs S3_ENDPOINT, AWS_ACCESS_KEY_ID,
|
|
AWS_SECRET_ACCESS_KEY in .env or the environment`. The message names only the missing ones.
|
|
- An endpoint without a scheme fails with
|
|
`S3_ENDPOINT must be a URL with a scheme (https://host[:port])`.
|
|
|
|
## What the store verifies at startup
|
|
|
|
`S3Store.verify_bucket()` runs once per process for each bucket and prefix. It does four things:
|
|
|
|
1. It reads the versioning status.
|
|
2. It reads the lifecycle rules.
|
|
3. It writes a two-line probe object at `_probe/select.jsonl` (under the prefix, if any).
|
|
4. It queries the probe with S3 Select and reads its tags.
|
|
|
|
Every missing piece is collected into one error:
|
|
|
|
```text
|
|
memory s3://sast-memory: the bucket is not set up for the store. The store never configures its bucket (an admin sets it once, the store verifies); missing:
|
|
- <one line per missing piece>
|
|
```
|
|
|
|
The table lists the lines exactly as the code writes them for this bucket at the root. `<failure>` is
|
|
the server's error code and the first 200 characters of its message.
|
|
|
|
| Check | Refusal line |
|
|
| --- | --- |
|
|
| versioning unreadable | `versioning cannot be read (<failure>): the agent needs s3:GetBucketVersioning on arn:aws:s3:::sast-memory, and versioning must be Enabled` |
|
|
| versioning off | `versioning is Off, it must be Enabled (provenance cites object versions). Admin: aws s3api put-bucket-versioning --endpoint-url "$S3_ENDPOINT" --bucket sast-memory --versioning-configuration Status=Enabled` (the actual status, e.g. `Suspended`, replaces `Off` when set) |
|
|
| lifecycle unreadable | `the lifecycle configuration cannot be read (<failure>): the agent needs s3:GetLifecycleConfiguration on arn:aws:s3:::sast-memory, and a rule must expire runs/` |
|
|
| a rule expires everything | `lifecycle rule(s) <rule ids> expire the whole store (repos/, facts/ and the index would be deleted); limit them to runs/` |
|
|
| no `runs/` rule | `no enabled lifecycle rule expires runs/ after a number of days. Admin (merged with any rules the bucket already has): aws s3api put-bucket-lifecycle-configuration --endpoint-url "$S3_ENDPOINT" --bucket sast-memory --lifecycle-configuration '{"Rules":[{"ID":"ousast-runs-expiry","Status":"Enabled","Filter":{"Prefix":"runs/"},"Expiration":{"Days":30}}]}'` |
|
|
| tags unreadable | `object tags cannot be read on _probe/select.jsonl (<failure>): the store filters row objects by their kind tag, so the agent needs s3:GetObjectTagging on arn:aws:s3:::sast-memory/*` |
|
|
| S3 Select missing | `S3 Select (SelectObjectContent over JSON Lines) does not answer on _probe/select.jsonl (<failure>). The store requires it and has no local fallback: use a server with S3 Select (RustFS has it; not every S3-compatible server does) and let the agent s3:PutObject and s3:GetObject` |
|
|
|
|
The admin commands inside the messages use `--endpoint-url "$S3_ENDPOINT"`. That is the full URL from
|
|
`.env`. So they run as written once `.env` is exported.
|
|
|
|
There is **no fallback**. The store does not:
|
|
|
|
- start in a degraded mode;
|
|
- drop to a local store;
|
|
- fetch and filter objects itself.
|
|
|
|
## The RustFS Select limit and the fixed-schema rule
|
|
|
|
RustFS's S3 Select infers an object's JSON schema from its leading rows. A `where` field absent from
|
|
those rows is unbound, even if a later row has it. The query then fails with
|
|
`EvaluatorBindingDoesNotExist` instead of matching (measured 2026-10-02 with the field first
|
|
present at row 5000).
|
|
|
|
The store does not read that failure as "no rows". That answer could silently drop matches. It raises:
|
|
|
|
```text
|
|
memory s3://sast-memory: S3 Select on <key> failed (EvaluatorBindingDoesNotExist: ...) (a field of [<fields>] is absent from the object's leading rows, which the server reads as its schema); the store has no local fallback
|
|
```
|
|
|
|
The rule that follows has two parts (see [Memory](memory.md#queryable-fields-per-kind)):
|
|
|
|
- A field the store filters on is present in every row of its kind from the first row on. It is `""`
|
|
where it does not apply.
|
|
- Each row kind has a fixed set of queryable fields. The store enforces it at write time.
|
|
|
|
## Checks against the real server
|
|
|
|
The contract tests that talk to a real server run only with `OUSAST_MEMORY_TEST_S3=1`. They use the
|
|
bucket `OUSAST_MEMORY_TEST_BUCKET`, else `S3_BUCKET`. The tests:
|
|
|
|
- verify the bucket at its root;
|
|
- write only under a fresh `contract-<id>/` prefix;
|
|
- remove every version and delete marker under it afterwards;
|
|
- never configure the bucket.
|
|
|
|
More on the host setup: [ax on this host](ops/ax/README.md).
|
|
|
|
## Scoped account for engine workers
|
|
|
|
Create a **new account** for the k3s engine Deployment. Attach only this policy.
|
|
Do not give it the existing memory-store account's policies.
|
|
|
|
```json
|
|
{
|
|
"Version": "2012-10-17",
|
|
"Statement": [
|
|
{
|
|
"Sid": "ListEnginePrefixes",
|
|
"Effect": "Allow",
|
|
"Action": "s3:ListBucket",
|
|
"Resource": "arn:aws:s3:::sast-memory",
|
|
"Condition": {
|
|
"StringLike": {
|
|
"s3:prefix": ["engine-queue/", "engine-queue/*", "engine-results/", "engine-results/*"]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
"Sid": "EngineObjectsOnly",
|
|
"Effect": "Allow",
|
|
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
|
|
"Resource": [
|
|
"arn:aws:s3:::sast-memory/engine-queue/*",
|
|
"arn:aws:s3:::sast-memory/engine-results/*"
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
The cluster can read and write only queued engine inputs, claims, and results.
|
|
It cannot list or read the rest of the memory store. It cannot change bucket
|
|
settings. The host queue dispatcher can use the same scoped account. Store its
|
|
keys in the `ousast-engine-s3` Secret; no key values belong in manifests.
|
|
Deployment and coordinator commands are in `ops/k8s/engine/README.md`.
|