mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 03:49:26 +00:00
pre-push(17.6): install, Docker wrapper and first-run docs
`ousast pre-push install|uninstall [REPO] [--force] [--docker]` ships in the wheel with the hook as package data; it honours core.hooksPath, refuses an existing hook unless --force (which chains it as pre-push.before-ousast), and uninstall restores the chained hook. ops/ousast-docker is a hook-callable script: the repository read-only and the artifact directory at their own paths, stdin passed through, no network, --memory 3g, the GHCR 2.0.1 image by default, advisory exit unless blocking. Help text for every pre-push flag; a first-run guide, settings, skip-reason glossary, CI recipe and full --help in docs/scanning.md; README tiers 1 and 2 pull the published image. The image was not rebuilt: 1.1 GB free, below the 4 GB threshold. Requirements 7.4, 9.6. 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
66357ffea1
commit
591818d5fc
@@ -654,10 +654,11 @@ Preserve all completed task evidence below.
|
||||
- Evidence: a hook push with `OUSAST_PUSH_ENGINE=background` followed by a second push showing the previous result.
|
||||
- _Done 2026-10-02:_ `--engine inline|background|off` (hook: `OUSAST_PUSH_ENGINE`) and `--background-deadline` (default 900 s). `off` and `background` run the quick tier and record `engine_off`/`engine_background` with a plain skip line; the deadline line names the deadline and the background option. New `push/background.py` starts one detached explicit replay (`start_new_session`, stdin closed, output to a log) per artifact directory and repository (lock file with a live-pid check: a second start reports busy), writes its artifact under `<artifact dir>/engine/<repo key>/<head>.json`, and the next run prints "Engine result for <head> from the previous push: ..." once, then marks it shown. Not usable through the Docker wrapper (the container exits with the hook); documented in 17.6. Tests: `tests/test_push_background.py` (4, one launching a real detached run). Gates: 2011 passed, 42 skipped; ruff, format, mypy clean.
|
||||
- _Requirements: 9.5, 4.1, 4.2_
|
||||
- [ ] 17.6 Install, Docker and documentation for a first-time user
|
||||
- [x] 17.6 Install, Docker and documentation for a first-time user
|
||||
- `ousast pre-push install|uninstall [--force] [--docker] [REPO]` ships the hook as package data, honours `core.hooksPath`, refuses an existing hook unless `--force` (which chains it as `pre-push.before-ousast`), and works from a pip install. `ops/ousast-docker` is a hook-callable script (not a shell function) that passes stdin and the artifact path through. The `openultrasast:dev` image is rebuilt only if `df -h /` shows at least 4 GB free; otherwise the rebuild command is documented and the record says it was not rebuilt. `docs/scanning.md` gains a first-run guide, a glossary of skip reasons, a GitHub Actions recipe and full `--help` text; README tiers 1 and 2 updated, pulling `ghcr.io/norandom/openultrasast:2.0.1` where the wrapper allows.
|
||||
- Files: `src/openultrasast/push/hook/pre-push`, `src/openultrasast/push/install.py` (new), `src/openultrasast/cli.py`, `ops/ousast-docker`, `docs/scanning.md`, `README.md`, `ops/README.md`. Tests: `tests/test_push_install.py` (new), `mkdocs build --strict`.
|
||||
- Evidence: install from the package into a scratch clone, push through it, uninstall restores the prior hook.
|
||||
- _Done 2026-10-02:_ `ousast pre-push install|uninstall [REPO] [--force] [--docker]` (`push/install.py`) installs the hook shipped as package data (`openultrasast/push/hook/pre-push`, byte-identical to `ops/pre-push`, verified in the built wheel), honours `core.hooksPath` without editing it, refuses an existing hook unless `--force` (ours is replaced; a foreign hook moves to `pre-push.before-ousast` and is chained), and uninstall removes only a hook it wrote and restores the chained one. `ops/ousast-docker` (also package data; `--docker` installs it beside the hook, which then uses it) is a script that mounts the repository read-only and the artifact directory at their own absolute paths, passes stdin with `-i`, runs as the user with no network and `--memory 3g`, defaults to `ghcr.io/norandom/openultrasast:2.0.1`, never pulls inside a hook, and exits 0 unless `OUSAST_PUSH_MODE=blocking`. The hook passes `--engine`/`--comparison-base` only when set, so an older ousast or image still runs. Every pre-push flag has help text; `docs/scanning.md` has the first-run guide, settings table, new-branch and background notes, the artifact keys, a skip-reason glossary (a test keeps it in sync with `push/explain.py`), a GitHub Actions recipe and the full `--help` text; README tiers 1 and 2 and `ops/README.md` updated (pull and tag the GHCR image instead of building). **Image not rebuilt:** `df -h /` showed 1.1 GB free (< 4 GB); rebuild with `docker build -t openultrasast:dev .`. Tests: `tests/test_push_install.py` (11) and the glossary test. Gates: 2022 passed, 42 skipped; ruff, format, mypy clean; `mkdocs build --strict` exit 0.
|
||||
- _Requirements: 9.6, 7.4_
|
||||
- [ ] 17.7 Prove it on the audit's three repositories
|
||||
- Rerun records, serve-static and unrolled/secure (same planted commits) with the new build in quick mode and with the hook installed, plus one Docker engine run on serve-static if free memory >= 3 GB and free disk >= 1 GB. Prove each run read its input (files, bytes).
|
||||
|
||||
@@ -58,6 +58,13 @@ uv run ousast scan . --mode quick --fail-on verified # CI: exit 1 on any evide
|
||||
This runs the language-scoped pattern rules, entry-point reachability hints, ranking,
|
||||
verification and scoring. It is deterministic and reproducible. It needs no network and no keys.
|
||||
|
||||
The same install gives you the advisory pre-push hook for any repository. It runs the quick rules
|
||||
on the lines a push changes, names every check it could not run, and never blocks a push:
|
||||
|
||||
```bash
|
||||
cd /path/to/your/repository && ousast pre-push install # remove: ousast pre-push uninstall
|
||||
```
|
||||
|
||||
What it cannot do:
|
||||
|
||||
- There is no engine. Nothing follows a value across statements or files.
|
||||
@@ -70,12 +77,20 @@ the overlay.
|
||||
|
||||
### Tier 2: standard and deep scans (Docker)
|
||||
|
||||
`standard` and `deep` need Joern. The Docker image `openultrasast:dev` ships three things in one
|
||||
image: the tool, Joern v4.0.625 and `php-cli` (the PHP frontend needs it). You build it locally
|
||||
from the `Dockerfile`; there is no published image yet. It is big: a JRE plus about 2 GB of Joern.
|
||||
`standard` and `deep` need Joern. The Docker image ships three things in one image: the tool,
|
||||
Joern v4.0.625 and `php-cli` (the PHP frontend needs it). Each release publishes it to GHCR. It is
|
||||
big: a JRE plus about 2 GB of Joern. Pull it instead of building, and tag it with the name the
|
||||
shell wrapper uses:
|
||||
|
||||
The shell wrapper builds the image on first use. Then it runs `ousast` in the container. Your
|
||||
directory is mounted read-only and the container network is off.
|
||||
```bash
|
||||
docker pull ghcr.io/norandom/openultrasast:2.0.1
|
||||
docker tag ghcr.io/norandom/openultrasast:2.0.1 openultrasast:dev # or build: docker compose build
|
||||
```
|
||||
|
||||
The shell wrapper runs `ousast` in the container. It builds the image only if `openultrasast:dev`
|
||||
is missing. Your directory is mounted read-only and the container network is off. For the pre-push
|
||||
hook, use `ousast pre-push install --docker`: Git cannot call a shell function, so it installs the
|
||||
script `ousast-docker` beside the hook.
|
||||
|
||||
```bash
|
||||
source /path/to/OpenUltraSAST/ops/shell/ousast.sh # PowerShell: ops/shell/ousast.ps1
|
||||
@@ -158,8 +173,11 @@ loads settings. Every run writes these files under `<target>/.openultrasast/runs
|
||||
and `trace/events.jsonl`.
|
||||
|
||||
`ousast pre-push` (experimental) analyses the commits a push would publish against their base.
|
||||
Only new or worsened defects become candidates. It is advisory by default. Its capability registry
|
||||
is empty, so it emits no normal alert today.
|
||||
First, the quick rules run on the changed lines. Then the engine compares head with base, and only
|
||||
new or worsened defects become candidates. It is advisory by default, and it prints one line per
|
||||
check it could not run. Its capability registry is empty, so it emits no normal alert today. An
|
||||
engine finding the push introduced is shown as advisory. Install, settings, skip reasons and a CI
|
||||
recipe: [docs/scanning.md](docs/scanning.md#first-run-install-the-hook-on-any-repository).
|
||||
|
||||
Flows and the per-language rule table: [docs/scanning.md](docs/scanning.md).
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"modules": 154,
|
||||
"modules": 155,
|
||||
"counts": {
|
||||
"load_bearing": 149,
|
||||
"load_bearing": 150,
|
||||
"standalone_capability": 5
|
||||
},
|
||||
"orphaned": [],
|
||||
@@ -1262,6 +1262,14 @@
|
||||
"push.runner"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "push.install",
|
||||
"classification": "load_bearing",
|
||||
"reason": "reachable from gate, map_gate, pair_gate, cli",
|
||||
"importers": [
|
||||
"cli"
|
||||
]
|
||||
},
|
||||
{
|
||||
"module": "push.policy",
|
||||
"classification": "load_bearing",
|
||||
|
||||
+274
-9
@@ -131,23 +131,288 @@ candidates. Sources: `ousast pre-push --help`, `src/openultrasast/push/`,
|
||||
flowchart TD
|
||||
input["git push refs on stdin, or --base / --head"] --> resolve["Resolve both commit identities"]
|
||||
resolve --> snap["Materialise tracked bytes in private scratch storage"]
|
||||
snap --> regions["Discover changed regions, up to --max-regions"]
|
||||
regions --> engine["Evidence ranker and arbiter on head"]
|
||||
snap --> quick["Quick rules on the changed head lines (advisory, under 1 s)"]
|
||||
quick --> eng{"--engine"}
|
||||
eng -- "off" --> art
|
||||
eng -- "background" --> bg["Detached engine run; result shown on the next push"]
|
||||
bg --> art
|
||||
eng -- "inline, the default" --> engine["Engine on head: changed regions up to --max-regions, ranker, arbiter"]
|
||||
engine --> cmp["Targeted base comparison: new or worsened only"]
|
||||
cmp --> admit{"Admission policy: capability registry"}
|
||||
admit -- "registry empty today" --> diag["Diagnostic candidates in the artifact"]
|
||||
admit -- "registry empty today" --> diag["Advisory engine findings, not alerts"]
|
||||
admit -- "an enabled capability" --> alert["Normal alert"]
|
||||
diag --> art["JSON artifact outside the repository"]
|
||||
diag --> art["Terminal lines + JSON artifact outside the repository"]
|
||||
alert --> art
|
||||
art --> exit{"--mode"}
|
||||
exit -- "advisory, the default" --> allow["Exit 0: allow"]
|
||||
exit -- "blocking, explicit opt-in" --> block["May block the push"]
|
||||
```
|
||||
|
||||
- `--deadline` (30 s by default) bounds preparation, both revisions and reporting together.
|
||||
- `--incomplete-coverage block` opts into failing when coverage is incomplete.
|
||||
- `--deadline` (30 s by default) bounds preparation, the quick rules, both revisions and reporting together.
|
||||
- The quick rules run first on the changed lines. Their matches are shown as advisory pattern
|
||||
matches, never as alerts, and never block.
|
||||
- The default capability registry is empty until independent qualification passes. So no normal
|
||||
alert is emitted today. Every candidate stays diagnostic.
|
||||
alert is emitted today. An engine finding the push introduced is shown as an advisory engine
|
||||
finding; every other candidate stays diagnostic in the artifact.
|
||||
- A model is called only with an explicit `--model-config`.
|
||||
- Hook installation (`ops/install-pre-push`) and the private cache (`--cache-dir`) are documented
|
||||
in [Engine and pre-push](ops/README.md#experimental-pre-push-integration).
|
||||
- The private cache (`--cache-dir`) and the manual install recipe are documented in
|
||||
[Engine and pre-push](ops/README.md#experimental-pre-push-integration).
|
||||
|
||||
### First run: install the hook on any repository
|
||||
|
||||
The hook is advisory. It never rejects a push unless you set `OUSAST_PUSH_MODE=blocking`. A check
|
||||
that cannot run says so in one line and lets the push through: a missing `ousast`, a missing
|
||||
Docker image, an invalid setting, a crash.
|
||||
|
||||
1. Install the tool. The package is not on PyPI. Install it from the repository (or from a
|
||||
release wheel on the GitHub releases page):
|
||||
|
||||
```bash
|
||||
pip install "git+https://github.com/norandom/OpenUltraSAST"
|
||||
```
|
||||
|
||||
2. Install the hook in the repository you want checked. Git's effective hook directory is used,
|
||||
including `core.hooksPath`, and that setting is never edited:
|
||||
|
||||
```bash
|
||||
cd /path/to/your/repository
|
||||
ousast pre-push install # refuses if a pre-push hook already exists
|
||||
ousast pre-push install --force # moves an existing hook to pre-push.before-ousast and chains it
|
||||
```
|
||||
|
||||
3. Push as usual. Without the Joern engine, you get the quick rules and one line saying the
|
||||
engine did not run. For the engine, choose one:
|
||||
|
||||
- a host install with `joern-parse` on the PATH (`pyinfra @local ops/joern.py`, see
|
||||
[Engine and pre-push](ops/README.md));
|
||||
- Docker: `docker pull ghcr.io/norandom/openultrasast:2.0.1`, then
|
||||
`ousast pre-push install --docker`. The wrapper `ousast-docker` is installed beside the
|
||||
hook and runs the check in the container (repository read-only, no network,
|
||||
`--memory 3g`). The container runs the code of its image's version: the quick-rule
|
||||
tier, the plain skip lines and `--engine` need an image built from a version that has them
|
||||
(`docker build -t openultrasast:dev .`, then `OUSAST_IMAGE=openultrasast:dev`).
|
||||
|
||||
4. Remove it with `ousast pre-push uninstall`. Only a hook this tool wrote is removed, and a
|
||||
chained previous hook is restored.
|
||||
|
||||
A push on the 2026-10-02 proof repository (serve-static, a new `debug.js` that evaluates
|
||||
`req.query.code`, no Joern on the host) prints:
|
||||
|
||||
```text
|
||||
Quick rules: 2 pattern match(es) on changed code (1 file(s), 87 bytes read). Advisory: not verified by the engine.
|
||||
- js-eval (CWE-95, critical) at debug.js:2: Dynamic JavaScript evaluation needs review
|
||||
- js-xss-response (CWE-79, medium) at debug.js:2: Unsanitized HTTP response reflection needs review
|
||||
No actionable defects reported; analysis is incomplete.
|
||||
Skipped: the changed code has no counterpart in the base revision to compare against
|
||||
Skipped: engine did not run: the code graph could not be built (is Joern installed? ...)
|
||||
Skipped: no base revision was analysed, so no engine finding can be called new or worsened
|
||||
Notice: Engine checks did not run. Install Joern, or run the hook through ops/ousast-docker. Review the details.
|
||||
Details: /home/you/.cache/openultrasast/push/push-12345.json
|
||||
```
|
||||
|
||||
The hook reads these settings from the environment of `git push`:
|
||||
|
||||
| Variable | Default | Effect |
|
||||
| --- | --- | --- |
|
||||
| `OUSAST_PUSH_DEADLINE` | `30` | seconds for the whole check |
|
||||
| `OUSAST_PUSH_MODE` | `advisory` | `blocking` is the only way a result can reject a push |
|
||||
| `OUSAST_INCOMPLETE_COVERAGE` | `allow` | with `blocking`: `block` also rejects incomplete coverage |
|
||||
| `OUSAST_PUSH_ENGINE` | `inline` | `background`: return after the quick rules, run the engine detached; `off`: quick rules only |
|
||||
| `OUSAST_COMPARISON_BASE` | unset | a local ref to compare a new branch against |
|
||||
| `OUSAST_ARTIFACT_DIR` | `~/.cache/openultrasast/push` | where the JSON results go; outside the repository |
|
||||
| `OUSAST_COMMAND` | `ousast` | the command the hook runs, for example the Docker wrapper |
|
||||
| `OUSAST_IMAGE` | `ghcr.io/norandom/openultrasast:2.0.1` | the Docker wrapper's image |
|
||||
| `OUSAST_DOCKER_MEMORY` | `3g` | the Docker wrapper's memory limit |
|
||||
|
||||
**A new branch.** Git gives the hook no remote base for a branch the remote does not have yet.
|
||||
The check says so ("new branch: the remote has no base to compare against"), runs the quick rules
|
||||
over the files the branch's last commit changed instead, and says that too. Set
|
||||
`OUSAST_COMPARISON_BASE=main` to compare a new branch against your local `main`.
|
||||
|
||||
**The engine and the deadline.** The engine does not fit a 30 s deadline today. The 2026-10-02
|
||||
audit measured 260 s on a 26-file repository; graph reuse is the planned fix. When it cannot
|
||||
finish, one line says so. With `OUSAST_PUSH_ENGINE=background`, the hook returns after the quick
|
||||
rules and starts one detached engine run (`--background-deadline`, 900 s by default). The next push
|
||||
in the same repository prints `Engine result for <head> from the previous push: ...` once. Only
|
||||
one background run happens at a time. The Docker wrapper cannot run the engine in the background,
|
||||
because the container ends with the hook.
|
||||
|
||||
**The artifact.** Every run writes one JSON file, and the terminal prints its path after
|
||||
`Details:`. It records the following:
|
||||
|
||||
- `snapshots[].bytes_read`: proof that the input was read;
|
||||
- `quick_tier`: the quick-rule matches, with the files and bytes read;
|
||||
- `admission.coverage_reasons`: the raw skip reasons;
|
||||
- `admission.dispositions`: every engine candidate and its vetoes;
|
||||
- `engine_background`: background runs started or shown;
|
||||
- `timings`.
|
||||
|
||||
Files accumulate, one per push; delete old ones when you like.
|
||||
|
||||
### Skip reasons
|
||||
|
||||
The terminal prints one `Skipped:` line per distinct reason in plain words. The artifact keeps the
|
||||
raw reason. Repository paths are hex-encoded there, and per-question reasons end in a hash.
|
||||
|
||||
| Raw reason (artifact) | Terminal line says | What to do |
|
||||
| --- | --- | --- |
|
||||
| `cpg_build_failed`, `cpg_unavailable` | the engine did not run: the code graph could not be built | install Joern, or use the Docker wrapper |
|
||||
| `cpg_empty` | the engine read no code | check that the changed files are in a supported language |
|
||||
| `frontend_unsupported` | no engine frontend for a language in this push | none: that language gets the quick rules at most |
|
||||
| `language_not_covered:<lang>:<changed>:<total>` | language not covered: `<lang>` (n changed files, m in the repository) | none: neither the engine nor the quick rules cover it, so silence is not a clean result |
|
||||
| `deadline_exhausted` | the engine did not finish within the deadline | raise `OUSAST_PUSH_DEADLINE`, or set `OUSAST_PUSH_ENGINE=background` |
|
||||
| `change_context_deadline_exhausted` | the deadline ran out while relating the change to the code | as above |
|
||||
| `report_deadline_exhausted` | the deadline ran out while saving the details | as above |
|
||||
| `engine_off`, `engine_background` | the engine was switched off, or runs after the push | none |
|
||||
| `resolution:missing_base` | new branch: the remote has no base to compare against | set `OUSAST_COMPARISON_BASE` |
|
||||
| `resolution:unsupported_base`, `resolution:no_merge_base`, `resolution:ambiguous_base`, `resolution:missing_target`, `resolution:unsupported_target`, `resolution:resolution_error` | the pushed ref or its remote base could not be resolved locally | fetch the remote, then push again |
|
||||
| `base_comparison_unavailable`, `base_unavailable` | no base revision was analysed, so nothing can be called new | see the line before it for the cause |
|
||||
| `base_counterpart_unresolved` | the changed code has no counterpart in the base to compare against | none: expected for new files |
|
||||
| `<path>:ambiguous_line_correspondence`, `<path>:line_correspondence_unavailable`, `<path>:ambiguous_path_rename_correspondence` | changed lines or a renamed file could not be matched to the base | none: findings there stay diagnostic |
|
||||
| `unmatched_added_deleted_path_correspondence` | files were both added and deleted, so a move cannot be told from a new file | push the move separately from other edits |
|
||||
| `declaration_paths_unavailable` | project declaration files could not be listed | rerun |
|
||||
| `capability_unavailable`, `capability_disabled`, `capability_unevaluated`, `capability_ambiguous` | engine findings are not qualified as alerts yet | none: they show as advisory engine findings |
|
||||
| `eligibility:<reason>` | the capability registry does not match this build | reinstall a consistent build |
|
||||
| `comparison_unknown`, `base_incomplete` and the other novelty reasons | whether this push introduced some engine findings could not be established | none: they stay diagnostic |
|
||||
| `dynamic_external_or_depth_context_unresolved:...` | n engine checks depend on dynamic or external code the engine cannot follow | none |
|
||||
| `query_failed`, `query_budget_reserved`, `query_too_expensive`, `regions_truncated`, `files_unparsed` | some engine questions were not answered | raise the deadline or `--max-regions` |
|
||||
| `cross_partition_semantics_unresolved` | flows between languages are not followed | none |
|
||||
| `dependency_unresolved`, `change_unsupported` | engine findings depend on unresolved code or have no supported link to the change | none |
|
||||
| `snapshot:<reason>` | some files could not be materialised for analysis | see the reason (symlinks, submodules, LFS) |
|
||||
| `<stage>_failed:<Error>`, `push_input_failed:<Error>` | the named step failed | rerun; report it if it repeats |
|
||||
|
||||
A reason not in this table is still printed as its own words, followed by "see the details file".
|
||||
|
||||
### In CI
|
||||
|
||||
The same check runs on a pushed range in CI. This GitHub Actions job is advisory: it never fails
|
||||
the build, and it uploads the artifact.
|
||||
|
||||
```yaml
|
||||
name: ousast pre-push (advisory)
|
||||
on: push
|
||||
jobs:
|
||||
pre-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- run: pip install "git+https://github.com/norandom/OpenUltraSAST"
|
||||
- name: Check what this push changed
|
||||
if: github.event.before != '0000000000000000000000000000000000000000'
|
||||
run: |
|
||||
ousast pre-push . --base ${{ github.event.before }} --head ${{ github.sha }} \
|
||||
--artifact "$RUNNER_TEMP/ousast-pre-push.json" --engine off --deadline 120 || true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: ousast-pre-push
|
||||
path: ${{ runner.temp }}/ousast-pre-push.json
|
||||
```
|
||||
|
||||
A branch's first push has no `github.event.before` to compare against, and the job skips it. For
|
||||
the engine in CI, run the job in the container image instead of `pip install`, and drop
|
||||
`--engine off`.
|
||||
|
||||
### `ousast pre-push --help`
|
||||
|
||||
```text
|
||||
usage: ousast pre-push [-h] [--base BASE] [--head HEAD] [--remote NAME URL]
|
||||
[--comparison-base COMPARISON_BASE] [--model-config MODEL_CONFIG]
|
||||
[--prior-hook PRIOR_HOOK] --artifact ARTIFACT [--cache-dir CACHE_DIR]
|
||||
[--deadline DEADLINE] [--cancellation-allowance CANCELLATION_ALLOWANCE]
|
||||
[--max-regions MAX_REGIONS] [--mode {advisory,blocking}]
|
||||
[--incomplete-coverage {allow,block}] [--engine {inline,background,off}]
|
||||
[--background-deadline BACKGROUND_DEADLINE]
|
||||
[--experimental-declarations EXPERIMENTAL_DECLARATIONS]
|
||||
[--experimental-record-vetoes]
|
||||
path
|
||||
|
||||
Check what a push changes: quick rules on the changed lines, then the Joern engine on head and
|
||||
base within one deadline. Advisory: exit 0 unless --mode blocking. Install the Git hook with
|
||||
'ousast pre-push install [REPO] [--force] [--docker]'; remove it with 'ousast pre-push uninstall
|
||||
[REPO]'.
|
||||
|
||||
positional arguments:
|
||||
path the repository to check (the hook passes '.')
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--base BASE explicit replay: the base revision (with --head; instead of --remote)
|
||||
--head HEAD explicit replay: the head revision to check against --base
|
||||
--remote NAME URL Git hook mode: the remote name and URL Git passes to pre-push; the push
|
||||
input is read from stdin
|
||||
--comparison-base COMPARISON_BASE
|
||||
hook mode: a local ref to compare a NEW branch against (the remote has no
|
||||
base for it); the hook reads OUSAST_COMPARISON_BASE
|
||||
--model-config MODEL_CONFIG
|
||||
explicit optional witness selection endpoint configuration
|
||||
--prior-hook PRIOR_HOOK
|
||||
explicitly chain an existing hook with the same input and arguments
|
||||
--artifact ARTIFACT where to write the complete JSON result; must be outside the repository
|
||||
--cache-dir CACHE_DIR
|
||||
reuse compatible local artifacts in a private directory outside the
|
||||
repository
|
||||
--deadline DEADLINE seconds for the whole check: resolution, quick rules, engine and reporting
|
||||
(default 30; hook: OUSAST_PUSH_DEADLINE)
|
||||
--cancellation-allowance CANCELLATION_ALLOWANCE
|
||||
seconds after the deadline to stop work and save the result (default 2)
|
||||
--max-regions MAX_REGIONS
|
||||
most code regions the engine examines, highest-ranked first (default 500)
|
||||
--mode {advisory,blocking}
|
||||
advisory (default) never blocks; blocking is the explicit opt-in that
|
||||
rejects a push for an admitted alert (hook: OUSAST_PUSH_MODE)
|
||||
--incomplete-coverage {allow,block}
|
||||
with --mode blocking only: also block when coverage is incomplete (hook:
|
||||
OUSAST_INCOMPLETE_COVERAGE)
|
||||
--engine {inline,background,off}
|
||||
inline (default): run the Joern engine inside the deadline; background:
|
||||
return after the quick rules and run the engine detached, showing its
|
||||
result on the next run; off: quick rules only
|
||||
--background-deadline BACKGROUND_DEADLINE
|
||||
seconds the detached engine run of --engine background may take (default
|
||||
900)
|
||||
--experimental-declarations EXPERIMENTAL_DECLARATIONS
|
||||
explicit --base/--head replay only: unreviewed capability declarations
|
||||
used to render the experimental evaluation explanation; never consulted by
|
||||
admission and cannot enable any capability
|
||||
--experimental-record-vetoes
|
||||
explicit --base/--head replay only: record each comparison and admission
|
||||
veto per raw finding instead of applying it; output is labeled
|
||||
experimental, changes no result and enables no hook capability
|
||||
```
|
||||
|
||||
```text
|
||||
usage: ousast pre-push install [-h] [--force] [--docker] [repository]
|
||||
|
||||
Install the advisory OpenUltraSAST pre-push hook into a repository's effective hook directory
|
||||
(core.hooksPath is honoured, never edited).
|
||||
|
||||
positional arguments:
|
||||
repository the repository (default: current directory)
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--force install: replace our hook, or move a foreign hook to pre-push.before-ousast and
|
||||
chain it; uninstall: remove a hook this tool did not write
|
||||
--docker also install the ousast-docker wrapper beside the hook, so the check runs in the
|
||||
container image
|
||||
```
|
||||
|
||||
```text
|
||||
usage: ousast pre-push uninstall [-h] [--force] [repository]
|
||||
|
||||
Remove the OpenUltraSAST pre-push hook and restore a chained previous hook.
|
||||
|
||||
positional arguments:
|
||||
repository the repository (default: current directory)
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--force install: replace our hook, or move a foreign hook to pre-push.before-ousast and
|
||||
chain it; uninstall: remove a hook this tool did not write
|
||||
```
|
||||
|
||||
+9
-2
@@ -291,13 +291,20 @@ The default capability registry is empty pending independent qualification. Inst
|
||||
this interface does not make the project rollout-ready. Advisory is the default;
|
||||
blocking and strict incomplete-coverage handling are separate explicit choices.
|
||||
|
||||
Put the installed `ousast` executable on the Git process's PATH. From this tool's
|
||||
checkout, explicitly install into the desired repository:
|
||||
The supported install is `ousast pre-push install [REPO] [--force] [--docker]`, shipped in
|
||||
the wheel; `ousast pre-push uninstall [REPO]` removes it. The first-run guide, settings, skip
|
||||
reasons and a CI recipe are in [Scanning](../scanning.md#first-run-install-the-hook-on-any-repository).
|
||||
If `ousast` is not on the Git process's PATH, the hook prints one line and lets the push through.
|
||||
From this tool's checkout, the script installer remains available:
|
||||
|
||||
```sh
|
||||
ops/install-pre-push /absolute/path/to/repository
|
||||
```
|
||||
|
||||
`ops/ousast-docker` is the hook-callable Docker wrapper (`OUSAST_COMMAND=/path/to/ops/ousast-docker`,
|
||||
or installed beside the hook by `--docker`). It is a script, not a shell function, so Git can run
|
||||
it. It passes the push input through and mounts the artifact directory at its own path.
|
||||
|
||||
The installer resolves Git's effective hook directory (including `core.hooksPath`),
|
||||
leaves that configuration untouched, and refuses to overwrite any existing hook.
|
||||
For an existing hook, inspect it and explicitly move it aside **before** installation:
|
||||
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
# OpenUltraSAST in Docker, callable from a Git hook. A script, not a shell function: Git runs
|
||||
# hooks non-interactively and never sees functions from ~/.bashrc.
|
||||
#
|
||||
# OUSAST_COMMAND=/path/to/ousast-docker git push # or: ousast pre-push install --docker
|
||||
#
|
||||
# The repository is mounted READ-ONLY at its own absolute path and the artifact directory
|
||||
# read-write at its own path, so every path in the arguments and in the output ("Details: ...")
|
||||
# means the same thing inside and outside the container. The push input on stdin is passed
|
||||
# through (-i). The container runs as your user, with no network and --memory 3g.
|
||||
#
|
||||
# Advisory like the hook: when Docker or the image is missing, it says the check did not run
|
||||
# and exits 0. Pull the image once first; a hook never downloads ~3 GB on its own:
|
||||
#
|
||||
# docker pull ghcr.io/norandom/openultrasast:2.0.1
|
||||
#
|
||||
# --engine background does not work through this wrapper: the container ends with the hook.
|
||||
image=${OUSAST_IMAGE:-ghcr.io/norandom/openultrasast:2.0.1}
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "ousast-docker: check did not run: docker is not installed. Push continues."
|
||||
exit 0
|
||||
fi
|
||||
if ! docker image inspect "$image" >/dev/null 2>&1; then
|
||||
echo "ousast-docker: check did not run: image $image is not present." \
|
||||
"Run 'docker pull $image' once. Push continues."
|
||||
exit 0
|
||||
fi
|
||||
repo=$(pwd -P)
|
||||
artifact_dir=${OUSAST_ARTIFACT_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/openultrasast/push}
|
||||
previous=
|
||||
for argument in "$@"; do
|
||||
if [ "$previous" = "--artifact" ]; then
|
||||
artifact_dir=$(dirname -- "$argument")
|
||||
fi
|
||||
previous=$argument
|
||||
done
|
||||
mkdir -p -- "$artifact_dir" || exit 0
|
||||
artifact_dir=$(cd -- "$artifact_dir" && pwd -P)
|
||||
# A linked worktree keeps its objects in the main repository's git directory: mount that too.
|
||||
common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || echo "$repo/.git")
|
||||
case "$common" in
|
||||
"$repo"/*) extra_mount= ;;
|
||||
*) extra_mount="-v $common:$common:ro" ;;
|
||||
esac
|
||||
# shellcheck disable=SC2086 # OUSAST_DOCKER_ARGS and extra_mount are deliberately word-split
|
||||
docker run --rm -i --network none --memory "${OUSAST_DOCKER_MEMORY:-3g}" \
|
||||
--user "$(id -u):$(id -g)" -e HOME=/tmp -e TMPDIR=/work --tmpfs /work:size=4g \
|
||||
-v "$repo:$repo:ro" -w "$repo" -v "$artifact_dir:$artifact_dir" $extra_mount \
|
||||
${OUSAST_DOCKER_ARGS:-} "$image" "$@"
|
||||
status=$?
|
||||
if [ "$status" -ge 125 ]; then
|
||||
echo "ousast-docker: check did not run: the container could not start (docker exit $status). Push continues."
|
||||
exit 0
|
||||
fi
|
||||
# Only an explicit blocking opt-in may turn the container's exit into a rejected push; an older
|
||||
# image that rejects a newer argument must not block either.
|
||||
if [ "$status" -ne 0 ] && [ "${OUSAST_PUSH_MODE:-advisory}" != blocking ]; then
|
||||
echo "ousast-docker: the check exited with status $status; push continues (advisory)."
|
||||
exit 0
|
||||
fi
|
||||
exit "$status"
|
||||
+10
-2
@@ -5,6 +5,11 @@
|
||||
# A prior hook is chained only after it was moved to the sibling path "$0.before-ousast".
|
||||
artifact_dir=${OUSAST_ARTIFACT_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/openultrasast/push}
|
||||
prior="$0.before-ousast"
|
||||
# The Docker wrapper installed beside this hook (`ousast pre-push install --docker`) is used
|
||||
# unless OUSAST_COMMAND names another command; otherwise `ousast` from PATH.
|
||||
if [ -z "${OUSAST_COMMAND:-}" ] && [ -x "$(dirname -- "$0")/ousast-docker" ]; then
|
||||
OUSAST_COMMAND="$(dirname -- "$0")/ousast-docker"
|
||||
fi
|
||||
command=${OUSAST_COMMAND:-ousast}
|
||||
if ! command -v "$command" >/dev/null 2>&1; then
|
||||
echo "ousast: pre-push check did not run: '$command' is not on PATH." \
|
||||
@@ -19,8 +24,11 @@ remote_url=$2
|
||||
set -- pre-push . --artifact "$artifact_dir/push-$$.json" \
|
||||
--deadline "${OUSAST_PUSH_DEADLINE:-30}" \
|
||||
--mode "${OUSAST_PUSH_MODE:-advisory}" \
|
||||
--incomplete-coverage "${OUSAST_INCOMPLETE_COVERAGE:-allow}" \
|
||||
--engine "${OUSAST_PUSH_ENGINE:-inline}"
|
||||
--incomplete-coverage "${OUSAST_INCOMPLETE_COVERAGE:-allow}"
|
||||
# Optional settings are passed only when set, so an older ousast (or image) still runs.
|
||||
if [ -n "${OUSAST_PUSH_ENGINE:-}" ]; then
|
||||
set -- "$@" --engine "$OUSAST_PUSH_ENGINE"
|
||||
fi
|
||||
if [ -n "${OUSAST_COMPARISON_BASE:-}" ]; then
|
||||
set -- "$@" --comparison-base "$OUSAST_COMPARISON_BASE"
|
||||
fi
|
||||
|
||||
+95
-13
@@ -132,28 +132,71 @@ def main(argv: list[str] | None = None) -> int:
|
||||
|
||||
|
||||
def _main(argv: list[str] | None) -> int:
|
||||
if (argv if argv is not None else sys.argv[1:])[:1] != ["pre-push"]:
|
||||
given = list(argv if argv is not None else sys.argv[1:])
|
||||
if given[:1] == ["pre-push"] and given[1:2] in (["install"], ["uninstall"]):
|
||||
return _hook_install(given[1], given[2:])
|
||||
if given[:1] != ["pre-push"]:
|
||||
load_dotenv()
|
||||
parser = argparse.ArgumentParser(prog="ousast")
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
replay_parser = subparsers.add_parser("pre-push", help="check Git push input or replay local revisions (experimental)")
|
||||
replay_parser.add_argument("path", type=Path)
|
||||
replay_parser.add_argument("--base")
|
||||
replay_parser.add_argument("--head")
|
||||
replay_parser.add_argument("--remote", nargs=2, metavar=("NAME", "URL"))
|
||||
replay_parser.add_argument("--comparison-base")
|
||||
replay_parser = subparsers.add_parser(
|
||||
"pre-push",
|
||||
help="check Git push input or replay local revisions (experimental, advisory)",
|
||||
description="Check what a push changes: quick rules on the changed lines, then the Joern engine on head and base "
|
||||
"within one deadline. Advisory: exit 0 unless --mode blocking. Install the Git hook with "
|
||||
"'ousast pre-push install [REPO] [--force] [--docker]'; remove it with 'ousast pre-push uninstall [REPO]'.",
|
||||
)
|
||||
replay_parser.add_argument("path", type=Path, help="the repository to check (the hook passes '.')")
|
||||
replay_parser.add_argument("--base", help="explicit replay: the base revision (with --head; instead of --remote)")
|
||||
replay_parser.add_argument("--head", help="explicit replay: the head revision to check against --base")
|
||||
replay_parser.add_argument(
|
||||
"--remote",
|
||||
nargs=2,
|
||||
metavar=("NAME", "URL"),
|
||||
help="Git hook mode: the remote name and URL Git passes to pre-push; the push input is read from stdin",
|
||||
)
|
||||
replay_parser.add_argument(
|
||||
"--comparison-base",
|
||||
help="hook mode: a local ref to compare a NEW branch against (the remote has no base for it); "
|
||||
"the hook reads OUSAST_COMPARISON_BASE",
|
||||
)
|
||||
replay_parser.add_argument("--model-config", type=Path, help="explicit optional witness selection endpoint configuration")
|
||||
replay_parser.add_argument("--prior-hook", type=Path, help="explicitly chain an existing hook with the same input and arguments")
|
||||
replay_parser.add_argument("--artifact", type=Path, required=True)
|
||||
replay_parser.add_argument(
|
||||
"--artifact", type=Path, required=True, help="where to write the complete JSON result; must be outside the repository"
|
||||
)
|
||||
replay_parser.add_argument(
|
||||
"--cache-dir", type=Path, help="reuse compatible local artifacts in a private directory outside the repository"
|
||||
)
|
||||
replay_parser.add_argument("--deadline", type=float, default=30.0)
|
||||
replay_parser.add_argument("--cancellation-allowance", type=float, default=2.0)
|
||||
replay_parser.add_argument("--max-regions", type=int, default=500)
|
||||
replay_parser.add_argument("--mode", choices=("advisory", "blocking"), default="advisory")
|
||||
replay_parser.add_argument("--incomplete-coverage", choices=("allow", "block"), default="allow")
|
||||
replay_parser.add_argument(
|
||||
"--deadline",
|
||||
type=float,
|
||||
default=30.0,
|
||||
help="seconds for the whole check: resolution, quick rules, engine and reporting (default 30; hook: OUSAST_PUSH_DEADLINE)",
|
||||
)
|
||||
replay_parser.add_argument(
|
||||
"--cancellation-allowance",
|
||||
type=float,
|
||||
default=2.0,
|
||||
help="seconds after the deadline to stop work and save the result (default 2)",
|
||||
)
|
||||
replay_parser.add_argument(
|
||||
"--max-regions", type=int, default=500, help="most code regions the engine examines, highest-ranked first (default 500)"
|
||||
)
|
||||
replay_parser.add_argument(
|
||||
"--mode",
|
||||
choices=("advisory", "blocking"),
|
||||
default="advisory",
|
||||
help="advisory (default) never blocks; blocking is the explicit opt-in that rejects a push for an admitted "
|
||||
"alert (hook: OUSAST_PUSH_MODE)",
|
||||
)
|
||||
replay_parser.add_argument(
|
||||
"--incomplete-coverage",
|
||||
choices=("allow", "block"),
|
||||
default="allow",
|
||||
help="with --mode blocking only: also block when coverage is incomplete (hook: OUSAST_INCOMPLETE_COVERAGE)",
|
||||
)
|
||||
replay_parser.add_argument(
|
||||
"--engine",
|
||||
choices=("inline", "background", "off"),
|
||||
@@ -421,6 +464,45 @@ def _main(argv: list[str] | None) -> int:
|
||||
return 2
|
||||
|
||||
|
||||
def _hook_install(action: str, argv: list[str]) -> int:
|
||||
"""`ousast pre-push install|uninstall [REPO]` from the hook shipped in the package (task 17.6)."""
|
||||
from .push.install import install, uninstall
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog=f"ousast pre-push {action}",
|
||||
description="Install the advisory OpenUltraSAST pre-push hook into a repository's effective hook directory "
|
||||
"(core.hooksPath is honoured, never edited)."
|
||||
if action == "install"
|
||||
else "Remove the OpenUltraSAST pre-push hook and restore a chained previous hook.",
|
||||
)
|
||||
parser.add_argument("repository", nargs="?", type=Path, default=Path("."), help="the repository (default: current directory)")
|
||||
parser.add_argument(
|
||||
"--force",
|
||||
action="store_true",
|
||||
help="install: replace our hook, or move a foreign hook to pre-push.before-ousast and chain it; "
|
||||
"uninstall: remove a hook this tool did not write",
|
||||
)
|
||||
if action == "install":
|
||||
parser.add_argument(
|
||||
"--docker",
|
||||
action="store_true",
|
||||
help="also install the ousast-docker wrapper beside the hook, so the check runs in the container image",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
try:
|
||||
code, lines = (
|
||||
install(args.repository, force=args.force, docker=args.docker)
|
||||
if action == "install"
|
||||
else uninstall(args.repository, force=args.force)
|
||||
)
|
||||
except (OSError, ValueError, subprocess.SubprocessError) as error:
|
||||
print(f"ousast pre-push {action}: {error}", file=sys.stderr)
|
||||
return 1
|
||||
for line in lines:
|
||||
print(line, file=sys.stderr if code else sys.stdout)
|
||||
return code
|
||||
|
||||
|
||||
def _advisory_hook(argv: list[str]) -> bool:
|
||||
"""A Git hook invocation (`--remote NAME URL`, stdin input) that did not opt into blocking."""
|
||||
if argv[:1] != ["pre-push"] or "--remote" not in argv:
|
||||
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/sh
|
||||
# OpenUltraSAST in Docker, callable from a Git hook. A script, not a shell function: Git runs
|
||||
# hooks non-interactively and never sees functions from ~/.bashrc.
|
||||
#
|
||||
# OUSAST_COMMAND=/path/to/ousast-docker git push # or: ousast pre-push install --docker
|
||||
#
|
||||
# The repository is mounted READ-ONLY at its own absolute path and the artifact directory
|
||||
# read-write at its own path, so every path in the arguments and in the output ("Details: ...")
|
||||
# means the same thing inside and outside the container. The push input on stdin is passed
|
||||
# through (-i). The container runs as your user, with no network and --memory 3g.
|
||||
#
|
||||
# Advisory like the hook: when Docker or the image is missing, it says the check did not run
|
||||
# and exits 0. Pull the image once first; a hook never downloads ~3 GB on its own:
|
||||
#
|
||||
# docker pull ghcr.io/norandom/openultrasast:2.0.1
|
||||
#
|
||||
# --engine background does not work through this wrapper: the container ends with the hook.
|
||||
image=${OUSAST_IMAGE:-ghcr.io/norandom/openultrasast:2.0.1}
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
echo "ousast-docker: check did not run: docker is not installed. Push continues."
|
||||
exit 0
|
||||
fi
|
||||
if ! docker image inspect "$image" >/dev/null 2>&1; then
|
||||
echo "ousast-docker: check did not run: image $image is not present." \
|
||||
"Run 'docker pull $image' once. Push continues."
|
||||
exit 0
|
||||
fi
|
||||
repo=$(pwd -P)
|
||||
artifact_dir=${OUSAST_ARTIFACT_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/openultrasast/push}
|
||||
previous=
|
||||
for argument in "$@"; do
|
||||
if [ "$previous" = "--artifact" ]; then
|
||||
artifact_dir=$(dirname -- "$argument")
|
||||
fi
|
||||
previous=$argument
|
||||
done
|
||||
mkdir -p -- "$artifact_dir" || exit 0
|
||||
artifact_dir=$(cd -- "$artifact_dir" && pwd -P)
|
||||
# A linked worktree keeps its objects in the main repository's git directory: mount that too.
|
||||
common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || echo "$repo/.git")
|
||||
case "$common" in
|
||||
"$repo"/*) extra_mount= ;;
|
||||
*) extra_mount="-v $common:$common:ro" ;;
|
||||
esac
|
||||
# shellcheck disable=SC2086 # OUSAST_DOCKER_ARGS and extra_mount are deliberately word-split
|
||||
docker run --rm -i --network none --memory "${OUSAST_DOCKER_MEMORY:-3g}" \
|
||||
--user "$(id -u):$(id -g)" -e HOME=/tmp -e TMPDIR=/work --tmpfs /work:size=4g \
|
||||
-v "$repo:$repo:ro" -w "$repo" -v "$artifact_dir:$artifact_dir" $extra_mount \
|
||||
${OUSAST_DOCKER_ARGS:-} "$image" "$@"
|
||||
status=$?
|
||||
if [ "$status" -ge 125 ]; then
|
||||
echo "ousast-docker: check did not run: the container could not start (docker exit $status). Push continues."
|
||||
exit 0
|
||||
fi
|
||||
# Only an explicit blocking opt-in may turn the container's exit into a rejected push; an older
|
||||
# image that rejects a newer argument must not block either.
|
||||
if [ "$status" -ne 0 ] && [ "${OUSAST_PUSH_MODE:-advisory}" != blocking ]; then
|
||||
echo "ousast-docker: the check exited with status $status; push continues (advisory)."
|
||||
exit 0
|
||||
fi
|
||||
exit "$status"
|
||||
Executable
+38
@@ -0,0 +1,38 @@
|
||||
#!/bin/sh
|
||||
# OpenUltraSAST pre-push safety net. Advisory: nothing here rejects a push unless
|
||||
# OUSAST_PUSH_MODE=blocking is set explicitly. Install with `ousast pre-push install`
|
||||
# (or ops/install-pre-push from a checkout); remove with `ousast pre-push uninstall`.
|
||||
# A prior hook is chained only after it was moved to the sibling path "$0.before-ousast".
|
||||
artifact_dir=${OUSAST_ARTIFACT_DIR:-${XDG_CACHE_HOME:-$HOME/.cache}/openultrasast/push}
|
||||
prior="$0.before-ousast"
|
||||
# The Docker wrapper installed beside this hook (`ousast pre-push install --docker`) is used
|
||||
# unless OUSAST_COMMAND names another command; otherwise `ousast` from PATH.
|
||||
if [ -z "${OUSAST_COMMAND:-}" ] && [ -x "$(dirname -- "$0")/ousast-docker" ]; then
|
||||
OUSAST_COMMAND="$(dirname -- "$0")/ousast-docker"
|
||||
fi
|
||||
command=${OUSAST_COMMAND:-ousast}
|
||||
if ! command -v "$command" >/dev/null 2>&1; then
|
||||
echo "ousast: pre-push check did not run: '$command' is not on PATH." \
|
||||
"Install it with 'pip install openultrasast' or set OUSAST_COMMAND. Push continues."
|
||||
if [ -e "$prior" ] || [ -L "$prior" ]; then
|
||||
exec "$prior" "$@"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
remote_name=$1
|
||||
remote_url=$2
|
||||
set -- pre-push . --artifact "$artifact_dir/push-$$.json" \
|
||||
--deadline "${OUSAST_PUSH_DEADLINE:-30}" \
|
||||
--mode "${OUSAST_PUSH_MODE:-advisory}" \
|
||||
--incomplete-coverage "${OUSAST_INCOMPLETE_COVERAGE:-allow}"
|
||||
# Optional settings are passed only when set, so an older ousast (or image) still runs.
|
||||
if [ -n "${OUSAST_PUSH_ENGINE:-}" ]; then
|
||||
set -- "$@" --engine "$OUSAST_PUSH_ENGINE"
|
||||
fi
|
||||
if [ -n "${OUSAST_COMPARISON_BASE:-}" ]; then
|
||||
set -- "$@" --comparison-base "$OUSAST_COMPARISON_BASE"
|
||||
fi
|
||||
if [ -e "$prior" ] || [ -L "$prior" ]; then
|
||||
set -- "$@" --prior-hook "$prior"
|
||||
fi
|
||||
exec "$command" "$@" --remote "$remote_name" "$remote_url"
|
||||
@@ -0,0 +1,117 @@
|
||||
"""`ousast pre-push install|uninstall`: the hook as package data (task 17.6, Requirements 7.4, 9.6).
|
||||
|
||||
Works from a pip install: the hook script and the Docker wrapper ship inside the wheel. Git's
|
||||
effective hook directory is resolved (including `core.hooksPath`) and never edited. An existing
|
||||
hook is never overwritten silently: without `--force` installation refuses; with it the existing
|
||||
hook moves to `pre-push.before-ousast` and is chained with the same input and arguments.
|
||||
Uninstall removes only a hook this tool wrote and restores the chained hook.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
|
||||
PRIOR_SUFFIX = ".before-ousast"
|
||||
# First lines this tool has written into an installed hook: the current one and the 2026-09 one.
|
||||
_MARKERS = (b"# OpenUltraSAST pre-push safety net.", b"# Explicitly copy using the non-overwriting recipe in ops/README.md.")
|
||||
|
||||
|
||||
def packaged(name: str) -> bytes:
|
||||
"""The shipped `pre-push` hook or `ousast-docker` wrapper."""
|
||||
return resources.files("openultrasast.push").joinpath("hook", name).read_bytes()
|
||||
|
||||
|
||||
def hooks_dir(repository: Path) -> Path:
|
||||
out = subprocess.run(
|
||||
["git", "-C", str(repository), "rev-parse", "--path-format=absolute", "--git-path", "hooks"],
|
||||
capture_output=True,
|
||||
check=False,
|
||||
timeout=30,
|
||||
)
|
||||
if out.returncode != 0:
|
||||
raise ValueError(f"{repository} is not a Git repository")
|
||||
return Path(os.fsdecode(out.stdout.rstrip(b"\n")))
|
||||
|
||||
|
||||
def ours(path: Path) -> bool:
|
||||
try:
|
||||
head = path.read_bytes()[:400]
|
||||
except OSError:
|
||||
return False
|
||||
return any(marker in head for marker in _MARKERS)
|
||||
|
||||
|
||||
def _write(path: Path, data: bytes) -> None:
|
||||
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o755)
|
||||
with os.fdopen(fd, "wb") as stream:
|
||||
stream.write(data)
|
||||
os.fchmod(stream.fileno(), 0o755)
|
||||
|
||||
|
||||
def _exists(path: Path) -> bool:
|
||||
return path.exists() or path.is_symlink()
|
||||
|
||||
|
||||
def install(repository: Path, *, force: bool = False, docker: bool = False) -> tuple[int, list[str]]:
|
||||
hooks = hooks_dir(repository)
|
||||
hooks.mkdir(parents=True, exist_ok=True)
|
||||
hook = hooks / "pre-push"
|
||||
prior = hooks / ("pre-push" + PRIOR_SUFFIX)
|
||||
wrapper = hooks / "ousast-docker"
|
||||
lines: list[str] = []
|
||||
if _exists(hook):
|
||||
if not force:
|
||||
what = "an OpenUltraSAST hook is already installed" if ours(hook) else "an existing pre-push hook"
|
||||
return 1, [
|
||||
f"Installation refused: {what} at {hook}.",
|
||||
"Rerun with --force to replace ours, or to move a foreign hook to pre-push.before-ousast and chain it.",
|
||||
]
|
||||
if ours(hook):
|
||||
hook.unlink()
|
||||
lines.append(f"Replaced the OpenUltraSAST hook at {hook}.")
|
||||
elif _exists(prior):
|
||||
return 1, [f"Installation refused: {prior} already holds a chained hook; resolve it by hand first."]
|
||||
else:
|
||||
os.rename(hook, prior)
|
||||
lines.append(f"Moved the existing hook to {prior}; it runs after the check with the same input.")
|
||||
if docker:
|
||||
if _exists(wrapper) and not (force and ours_wrapper(wrapper)):
|
||||
return 1, [f"Installation refused: {wrapper} exists; rerun with --force to replace it."]
|
||||
if _exists(wrapper):
|
||||
wrapper.unlink()
|
||||
_write(wrapper, packaged("ousast-docker"))
|
||||
lines.append(f"Installed the Docker wrapper {wrapper} (image: ghcr.io/norandom/openultrasast:2.0.1; pull it once).")
|
||||
_write(hook, packaged("pre-push"))
|
||||
lines.insert(0, f"Installed {hook}")
|
||||
lines.append("Advisory: the hook never blocks a push unless OUSAST_PUSH_MODE=blocking. Remove with 'ousast pre-push uninstall'.")
|
||||
return 0, lines
|
||||
|
||||
|
||||
def ours_wrapper(path: Path) -> bool:
|
||||
try:
|
||||
return b"OpenUltraSAST in Docker, callable from a Git hook" in path.read_bytes()[:400]
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def uninstall(repository: Path, *, force: bool = False) -> tuple[int, list[str]]:
|
||||
hooks = hooks_dir(repository)
|
||||
hook = hooks / "pre-push"
|
||||
prior = hooks / ("pre-push" + PRIOR_SUFFIX)
|
||||
wrapper = hooks / "ousast-docker"
|
||||
if not _exists(hook):
|
||||
return 1, [f"Nothing to remove: no pre-push hook at {hook}."]
|
||||
if not ours(hook) and not force:
|
||||
return 1, [f"Removal refused: {hook} was not written by OpenUltraSAST. Inspect it, or rerun with --force."]
|
||||
hook.unlink()
|
||||
lines = [f"Removed {hook}"]
|
||||
if _exists(wrapper) and ours_wrapper(wrapper):
|
||||
wrapper.unlink()
|
||||
lines.append(f"Removed {wrapper}")
|
||||
if _exists(prior):
|
||||
os.rename(prior, hook)
|
||||
lines.append(f"Restored the previous hook from {prior}.")
|
||||
return 0, lines
|
||||
@@ -39,6 +39,19 @@ def test_audit_reasons_become_one_plain_line_each():
|
||||
assert sum("could not be established" in line for line in lines) == 1
|
||||
|
||||
|
||||
def test_every_glossary_reason_is_documented_for_users():
|
||||
"""17.6: the skip-reason glossary in docs/scanning.md names every reason the terminal explains."""
|
||||
from pathlib import Path
|
||||
|
||||
from openultrasast.push.explain import _NOVELTY_REASONS, _RESOLUTION, GLOSSARY
|
||||
|
||||
docs = (Path(__file__).resolve().parents[1] / "docs" / "scanning.md").read_text()
|
||||
missing = [key for key in GLOSSARY if key not in docs]
|
||||
missing += [f"resolution:{key}" for key in _RESOLUTION if f"`resolution:{key}`" not in docs]
|
||||
assert not missing, missing
|
||||
assert "base_incomplete" in docs and _NOVELTY_REASONS
|
||||
|
||||
|
||||
def test_unknown_reason_is_still_printed():
|
||||
assert explain(("some_future_reason",)) == ["some future reason (see the details file)"]
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
"""`ousast pre-push install|uninstall` and the hook-callable Docker wrapper (task 17.6, Req 7.4, 9.6)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from test_push_runner import history
|
||||
|
||||
from openultrasast.cli import main
|
||||
from openultrasast.push.install import install, packaged, uninstall
|
||||
|
||||
OPS = Path(__file__).resolve().parents[1] / "ops"
|
||||
|
||||
|
||||
def repo(tmp_path):
|
||||
root, base, head, git = history(tmp_path)
|
||||
git("config", "core.hooksPath", ".custom-hooks")
|
||||
remote = tmp_path / "remote.git"
|
||||
subprocess.run(["git", "init", "--bare", str(remote)], check=True, capture_output=True)
|
||||
git("remote", "add", "origin", str(remote))
|
||||
return root, git
|
||||
|
||||
|
||||
def test_packaged_hook_and_wrapper_are_the_ops_scripts():
|
||||
assert packaged("pre-push") == (OPS / "pre-push").read_bytes()
|
||||
assert packaged("ousast-docker") == (OPS / "ousast-docker").read_bytes()
|
||||
|
||||
|
||||
def test_install_uninstall_through_the_cli_honours_hooks_path(tmp_path, capsys):
|
||||
root, git = repo(tmp_path)
|
||||
assert main(["pre-push", "install", str(root)]) == 0
|
||||
hook = root / ".custom-hooks" / "pre-push"
|
||||
assert hook.read_bytes() == packaged("pre-push") and os.access(hook, os.X_OK)
|
||||
assert git("config", "core.hooksPath") == ".custom-hooks"
|
||||
assert main(["pre-push", "install", str(root)]) == 1 # already installed: refused without --force
|
||||
assert "already installed" in capsys.readouterr().err
|
||||
assert main(["pre-push", "install", str(root), "--force"]) == 0
|
||||
assert main(["pre-push", "uninstall", str(root)]) == 0
|
||||
assert not hook.exists()
|
||||
|
||||
|
||||
def test_existing_hook_is_refused_then_chained_and_restored(tmp_path):
|
||||
root, git = repo(tmp_path)
|
||||
hooks = root / ".custom-hooks"
|
||||
hooks.mkdir()
|
||||
log = tmp_path / "prior-input"
|
||||
original = f'#!/bin/sh\ncat > "{log}"\nexit 0\n'.encode()
|
||||
(hooks / "pre-push").write_bytes(original)
|
||||
(hooks / "pre-push").chmod(0o755)
|
||||
code, lines = install(root)
|
||||
assert code == 1 and "existing pre-push hook" in lines[0] and (hooks / "pre-push").read_bytes() == original
|
||||
code, lines = install(root, force=True)
|
||||
assert code == 0 and (hooks / "pre-push.before-ousast").read_bytes() == original
|
||||
env = {**os.environ, "OUSAST_COMMAND": "ousast-not-installed-anywhere", "OUSAST_ARTIFACT_DIR": str(tmp_path / "a")}
|
||||
pushed = subprocess.run(["git", "-C", str(root), "push", "origin", "HEAD:refs/heads/main"], env=env, capture_output=True)
|
||||
assert pushed.returncode == 0 and "refs/heads/main" in log.read_text()
|
||||
code, _ = uninstall(root)
|
||||
assert code == 0 and (hooks / "pre-push").read_bytes() == original
|
||||
assert not (hooks / "pre-push.before-ousast").exists()
|
||||
code, lines = uninstall(root)
|
||||
assert code == 1 and "not written by OpenUltraSAST" in lines[0]
|
||||
|
||||
|
||||
def fake_docker(tmp_path, *, inspect=0, run=0):
|
||||
bin_dir = tmp_path / "bin"
|
||||
bin_dir.mkdir()
|
||||
record = tmp_path / "docker-args"
|
||||
stdin = tmp_path / "docker-stdin"
|
||||
script = f'#!/bin/sh\nif [ "$1" = image ]; then exit {inspect}; fi\nprintf "%s\\n" "$@" > "{record}"\ncat > "{stdin}"\nexit {run}\n'
|
||||
(bin_dir / "docker").write_text(script)
|
||||
(bin_dir / "docker").chmod(0o755)
|
||||
return bin_dir, record, stdin
|
||||
|
||||
|
||||
def test_docker_install_routes_the_hook_through_the_wrapper_with_stdin_and_mounts(tmp_path):
|
||||
root, git = repo(tmp_path)
|
||||
assert install(root, docker=True)[0] == 0
|
||||
bin_dir, record, stdin = fake_docker(tmp_path)
|
||||
artifacts = tmp_path / "artifacts"
|
||||
env = {**os.environ, "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", "OUSAST_ARTIFACT_DIR": str(artifacts)}
|
||||
env.pop("OUSAST_COMMAND", None)
|
||||
pushed = subprocess.run(["git", "-C", str(root), "push", "origin", "HEAD:refs/heads/main"], env=env, capture_output=True)
|
||||
assert pushed.returncode == 0, pushed.stderr
|
||||
args = record.read_text().splitlines()
|
||||
real = str(root.resolve())
|
||||
assert args[:2] == ["run", "--rm"] and "-i" in args and "--network" in args and "none" in args
|
||||
assert f"{real}:{real}:ro" in args and str(artifacts.resolve()) + ":" + str(artifacts.resolve()) in args
|
||||
assert args[args.index("-w") + 1] == real
|
||||
assert "ghcr.io/norandom/openultrasast:2.0.1" in args and "pre-push" in args and "--remote" in args
|
||||
assert stdin.read_text().startswith("HEAD ") and "refs/heads/main" in stdin.read_text()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("inspect", "run", "needle"), [(1, 0, "is not present"), (0, 125, "could not start")])
|
||||
def test_wrapper_failures_never_block(tmp_path, inspect, run, needle):
|
||||
bin_dir, _, _ = fake_docker(tmp_path, inspect=inspect, run=run)
|
||||
env = {**os.environ, "PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}", "OUSAST_ARTIFACT_DIR": str(tmp_path / "artifacts")}
|
||||
result = subprocess.run(
|
||||
[str(OPS / "ousast-docker"), "pre-push", ".", "--artifact", str(tmp_path / "x" / "r.json")],
|
||||
env=env,
|
||||
cwd=tmp_path,
|
||||
input=b"",
|
||||
capture_output=True,
|
||||
)
|
||||
assert result.returncode == 0 and needle.encode() in result.stdout
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("mode", "status", "expected"), [("blocking", 1, 1), ("advisory", 1, 0), ("advisory", 2, 0)])
|
||||
def test_only_explicit_blocking_passes_a_nonzero_exit_through(tmp_path, mode, status, expected):
|
||||
bin_dir, _, _ = fake_docker(tmp_path, run=status)
|
||||
env = {
|
||||
**os.environ,
|
||||
"PATH": f"{bin_dir}{os.pathsep}{os.environ['PATH']}",
|
||||
"OUSAST_ARTIFACT_DIR": str(tmp_path / "artifacts"),
|
||||
"OUSAST_PUSH_MODE": mode,
|
||||
}
|
||||
result = subprocess.run([str(OPS / "ousast-docker"), "pre-push", "."], env=env, cwd=tmp_path, input=b"", capture_output=True)
|
||||
assert result.returncode == expected
|
||||
|
||||
|
||||
def test_hook_passes_optional_settings_only_when_set():
|
||||
text = (OPS / "pre-push").read_text()
|
||||
assert '--engine "$OUSAST_PUSH_ENGINE"' in text and 'if [ -n "${OUSAST_PUSH_ENGINE:-}" ]' in text
|
||||
Reference in New Issue
Block a user