mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-17 03:59:28 +00:00
docs: plain-language pass -- README, index, where we stand, scanning, examples, threat model
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
50f4e69321
commit
552671ca6a
+75
-52
@@ -1,78 +1,101 @@
|
||||
# Threat model & hardening
|
||||
|
||||
OpenUltraSAST analyzes **untrusted code**. This document states what it trusts, what
|
||||
it does not, and the controls that keep a scan safe to run in CI and safe to share.
|
||||
OpenUltraSAST analyzes **untrusted code**. This page states what it trusts and what it does not.
|
||||
It also lists the controls that keep a scan safe to run in CI and safe to share.
|
||||
|
||||
## Trust boundaries
|
||||
|
||||
- **Scanned code is untrusted input.** Quick and standard modes never import, build or run
|
||||
the target: quick reads source as text, and standard additionally parses it into a code
|
||||
property graph with Joern (`joern-parse`, a parser, not a build) and may send code excerpts
|
||||
to the configured model. Only `deep` mode executes target-derived code: its REGRESS stage
|
||||
loads promoted candidates inside the Docker sandbox below, and it runs only when `docker
|
||||
info` succeeds (otherwise the stage is skipped and recorded as `sandbox_unavailable`).
|
||||
- **The ruleset and CWE policy are trusted, governed data.** They change only through
|
||||
reviewed commits or the bounded self-improvement loop (`ousast improve`), which can
|
||||
flip rule *status* and tune score constants but can never edit pattern text or the
|
||||
- **Scanned code is untrusted input.** Quick and standard modes never import, build or run the
|
||||
target.
|
||||
- Quick mode reads source as text.
|
||||
- Standard mode also parses it into a code property graph (a graph of the program's syntax,
|
||||
control flow and data flow) with Joern. It uses `joern-parse`, a parser, not a build.
|
||||
Standard mode may send code excerpts to the configured model.
|
||||
- Only `deep` mode executes target-derived code. Its REGRESS stage loads promoted candidates
|
||||
inside the Docker sandbox described below. It runs only when `docker
|
||||
info` succeeds (otherwise the stage is skipped and recorded as `sandbox_unavailable`).
|
||||
- **The ruleset and CWE policy are trusted, governed data.** They change in only two ways:
|
||||
through reviewed commits, or through the bounded self-improvement loop (`ousast improve`). The
|
||||
loop can flip rule *status* and tune score constants. It can never edit pattern text or the
|
||||
authoritative 0–5 severity. See [architecture.md](architecture.md#self-improvement).
|
||||
- **Provider credentials are the operator's.** The core install (PyYAML only) makes no
|
||||
network calls on its own. A local scan calls a model only in standard or deep mode, and
|
||||
only when a key is available: the model layer asks the residual question the graph cannot
|
||||
settle, and `[models] hunter` enables the tool hunter. LLM calls go to DeepSeek
|
||||
(`DEEPSEEK_API_KEY`); OpenRouter (`OPENROUTER_API_KEY`) serves embeddings. Keys come from
|
||||
the environment or a gitignored `.env`, which never overrides an exported variable. The
|
||||
pre-push check calls a model only with an explicit `--model-config`. Agentic work runs on the plane (`ousast plane run`): `ousast` reads
|
||||
the variable a task's Model names (`secretKey.key`) from the operator's environment or
|
||||
`.env` and sends the key only in that task's start request, through Agent Substrate's
|
||||
router to that task's actor. It is not baked into an image or task template, and it is
|
||||
never logged or written to artifacts.
|
||||
- **Provider credentials are the operator's.** The core install (PyYAML only) makes no network
|
||||
calls on its own.
|
||||
- A local scan calls a model only in standard or deep mode, and only when a key is available.
|
||||
The model layer asks the residual question the graph cannot settle. `[models] hunter`
|
||||
enables the tool hunter.
|
||||
- LLM calls go to DeepSeek (`DEEPSEEK_API_KEY`). OpenRouter (`OPENROUTER_API_KEY`) serves
|
||||
embeddings.
|
||||
- Keys come from the environment or a gitignored `.env`. `.env` never overrides an exported
|
||||
variable.
|
||||
- The pre-push check calls a model only with an explicit `--model-config`.
|
||||
- Agentic work runs on the plane (`ousast plane run`). `ousast` reads the variable a task's
|
||||
Model names (`secretKey.key`) from the operator's environment or `.env`. It sends the key
|
||||
only in that task's start request. The request goes through Agent Substrate's router to that
|
||||
task's actor. The key is not baked into an image or task template. It is never logged or
|
||||
written to artifacts.
|
||||
|
||||
## Controls
|
||||
|
||||
### Secret redaction
|
||||
Scan artifacts can quote source that contains live credentials. `redaction.py` masks
|
||||
recognizable secret shapes (provider API keys, AWS/GitHub/Slack/Google tokens, bearer
|
||||
tokens, URL-embedded credentials, PEM private keys, and `key = value` assignments for
|
||||
sensitive names) before traces (`trace/events.jsonl`) and the markdown report are
|
||||
written. On by default; disable with `[hardening] redact_secrets = false`.
|
||||
recognizable secret shapes before traces (`trace/events.jsonl`) and the markdown report are
|
||||
written. It masks:
|
||||
|
||||
- provider API keys;
|
||||
- AWS/GitHub/Slack/Google tokens;
|
||||
- bearer tokens;
|
||||
- URL-embedded credentials;
|
||||
- PEM private keys;
|
||||
- `key = value` assignments for sensitive names.
|
||||
|
||||
Redaction is on by default. Turn it off with `[hardening] redact_secrets = false`.
|
||||
|
||||
### Plane egress
|
||||
Each plane task runs in its own actor behind Agent Substrate's egress gateway, which
|
||||
denies by default. The task's EgressPolicy (`src/openultrasast/plane/egress.py`) allows exactly three
|
||||
things: plain HTTP to the artifact receiver, and TLS passthrough to the Git hosts of the
|
||||
task's bound Workspaces and to the hosts its bound Model declares
|
||||
(`openultrasast.io/egress-hosts`). There are no wildcards, no IP addresses and no TLS
|
||||
interception. The policy is deleted with the actor.
|
||||
Each plane task runs in its own actor behind Agent Substrate's egress gateway. The gateway denies
|
||||
by default. The task's EgressPolicy (`src/openultrasast/plane/egress.py`) allows exactly three
|
||||
things:
|
||||
|
||||
1. plain HTTP to the artifact receiver;
|
||||
2. TLS passthrough to the Git hosts of the task's bound Workspaces;
|
||||
3. TLS passthrough to the hosts its bound Model declares (`openultrasast.io/egress-hosts`).
|
||||
|
||||
There are no wildcards, no IP addresses and no TLS interception. The policy is deleted with the
|
||||
actor.
|
||||
|
||||
### Cost & CI budgets
|
||||
- **Agentic spend** is bounded per plane task by the Run's `budget: {usd, calls}`. The
|
||||
metered client refuses the next call once either ceiling is reached (the task is
|
||||
`unfinished` and resumes on a rerun with a larger budget). A usd budget on a Model
|
||||
without prices is refused rather than metered as zero, and an account or
|
||||
authentication error fails the task and starts nothing further. The standard-mode tool
|
||||
hunter is bounded by the per-tier hunter budgets.
|
||||
- **Agentic spend** is bounded per plane task by the Run's `budget: {usd, calls}`.
|
||||
- The metered client refuses the next call once either ceiling is reached. The task is then
|
||||
`unfinished`. It resumes on a rerun with a larger budget.
|
||||
- A usd budget on a Model without prices is refused. It is not metered as zero.
|
||||
- An account or authentication error fails the task and starts nothing further.
|
||||
- The standard-mode tool hunter is bounded by the per-tier hunter budgets.
|
||||
- **Output size** is bounded by `[hardening] max_findings` (0 = unlimited). Truncation is
|
||||
severity-ordered and disclosed as a `budget` degradation in the manifest.
|
||||
severity-ordered. It is disclosed as a `budget` degradation in the manifest.
|
||||
|
||||
### Provider reliability
|
||||
LLM/embedding calls retry transient failures (HTTP 429/5xx, connection errors, timeouts)
|
||||
with exponential backoff; non-transient errors (e.g. 4xx, malformed JSON) fail fast.
|
||||
LLM/embedding calls retry transient failures with exponential backoff. Transient failures are
|
||||
HTTP 429/5xx, connection errors and timeouts. Non-transient errors fail fast, for example 4xx or
|
||||
malformed JSON.
|
||||
|
||||
### Visible degradation & determinism
|
||||
When an optional engine is missing (Joern: `cpg_unavailable`; Docker: `sandbox_unavailable`),
|
||||
the scan falls back to its deterministic equivalent and records a `degradations` entry in the
|
||||
manifest — it is never silently downgraded. Without a provider key the model layer asks no
|
||||
question and reports only what the graph entailed. Runs are reproducible: fixed config,
|
||||
artifact manifests, prompt hashes, and model identifiers are recorded.
|
||||
A scan is never silently downgraded. When an optional engine is missing, the scan falls back to
|
||||
its deterministic equivalent. It records a `degradations` entry in the manifest (Joern:
|
||||
`cpg_unavailable`; Docker: `sandbox_unavailable`).
|
||||
|
||||
Without a provider key, the model layer asks no question. It reports only what the graph
|
||||
entailed.
|
||||
|
||||
Runs are reproducible. The fixed config, artifact manifests, prompt hashes and model identifiers
|
||||
are recorded.
|
||||
|
||||
### MCP surface
|
||||
`ousast mcp` exposes only the ten narrow project tools. No tool runs arbitrary shell,
|
||||
Docker, or internal hunter tools, and no tool accepts a free-form command argument.
|
||||
`ousast mcp` exposes only the ten narrow project tools. No tool runs arbitrary shell, Docker or
|
||||
internal hunter tools. No tool accepts a free-form command argument.
|
||||
|
||||
## Sandbox (deep mode)
|
||||
|
||||
The REGRESS stage (`regress/`, `sandbox/runner.py`) runs each snippet with `docker run`
|
||||
and never mounts the host Docker socket:
|
||||
The REGRESS stage (`regress/`, `sandbox/runner.py`) runs each snippet with `docker run`. It never
|
||||
mounts the host Docker socket.
|
||||
|
||||
| Control | Setting |
|
||||
|---|---|
|
||||
@@ -84,5 +107,5 @@ and never mounts the host Docker socket:
|
||||
| pids | `[sandbox] pids_limit`, default 512 |
|
||||
| timeout | `[sandbox] timeout_seconds`, default 300 s |
|
||||
|
||||
Before anything runs, a structural deny-list (`regress/safety.py`) rejects a snippet that
|
||||
mentions sockets, curl, host networking or writes under `/workspace`.
|
||||
A structural deny-list (`regress/safety.py`) checks each snippet before anything runs. It rejects
|
||||
a snippet that mentions sockets, curl, host networking or writes under `/workspace`.
|
||||
|
||||
Reference in New Issue
Block a user