From adbb824eacfa1d012aab5235ece31549af74bff8 Mon Sep 17 00:00:00 2001 From: Marius Ciepluch <11855163+norandom@users.noreply.github.com> Date: Tue, 8 Sep 2026 19:37:58 +0200 Subject: [PATCH] feat(ops): a sourceable wrapper so docker is nobody's problem source ops/shell/ousast.sh # bash / zsh . ops\shell\ousast.ps1 # powershell ousast scan . ousast scan src/api The container becomes an implementation detail. The engine is a JVM and ~2 GB of Joern, and a contributor evaluating this tool should not have to install either to try it -- what they get is a command. Path rewriting is what makes it feel local: arguments under the working directory are mapped to their mounted equivalents, so `src/api` means what the user expects. Anything outside the mount is passed through unchanged WITH A WARNING rather than silently rewritten into a path the container cannot see -- a wrong path that looks like it worked is worse than one that complains. A native install wins when it is present. Someone who installed Joern deliberately should not be quietly routed through a container, so the wrapper uses a local joern-parse and venv when both exist; OUSAST_PREFER_NATIVE=0 forces the container. Verified end to end from an unrelated directory. The image builds itself on first use and says why it is pausing. A first run that prints a wall of build output and then works is worse than one that explains itself. Defaults stay conservative: the working directory mounts READ-ONLY, findings go to ./.ousast as the only writable mount, and the network is off unless `ousast-with-judge` asks for it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG --- docker-compose.yml | 4 +-- ops/README.md | 31 +++++++++++++++++ ops/shell/ousast.ps1 | 52 ++++++++++++++++++++++++++++ ops/shell/ousast.sh | 81 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 ops/shell/ousast.ps1 create mode 100644 ops/shell/ousast.sh diff --git a/docker-compose.yml b/docker-compose.yml index d9e0eb8..ff45b3f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,8 +15,8 @@ services: JOERN_VERSION: ${JOERN_VERSION:-v4.0.623} image: openultrasast:${OUSAST_VERSION:-dev} volumes: - - ${TARGET:-.}:/target:ro # the repository under analysis, read-only - - ./reports:/reports # where findings land, the one writable mount + - ${TARGET:-.}:/target:ro # the repository under analysis, read-only + - ${OUSAST_REPORTS:-./reports}:/reports # where findings land, the one writable mount working_dir: /work tmpfs: # CPG construction is scratch-heavy and none of it should outlive the run or touch the host. diff --git a/ops/README.md b/ops/README.md index 21f3fce..6994d64 100644 --- a/ops/README.md +++ b/ops/README.md @@ -3,6 +3,37 @@ Infrastructure for the optional engines. Nothing here is a project dependency: the core install is `dependencies = []` and every engine below degrades to a recorded reason when it is absent. +## Use it as a command + +Source the wrapper once and Docker stops being something you think about: + +```bash +# ~/.bashrc or ~/.zshrc +source /path/to/OpenUltraSAST/ops/shell/ousast.sh +``` + +```powershell +# $PROFILE +. C:\path\to\OpenUltraSAST\ops\shell\ousast.ps1 +``` + +Then, from any directory: + +```bash +ousast scan . +ousast scan src/api +ousast-with-judge scan . # opt into the LLM judge: needs network and a key +``` + +The wrapper rewrites paths under your working directory to their mounted equivalents, so `src/api` means what +you expect. Anything outside the mount is passed through unchanged with a warning rather than silently +rewritten into a path the container cannot see. Your directory is mounted **read-only**; findings go to +`./.ousast`, the one writable mount. The image builds itself on first use and says so. + +If `joern-parse` is already on your PATH and a local venv exists, the wrapper uses those instead — someone who +installed the engine deliberately should not be quietly routed through a container. Set +`OUSAST_PREFER_NATIVE=0` to always containerise. + ## Two ways to get the engine **Docker (recommended for contributors).** The tool and Joern ship in one image, so nothing is installed on diff --git a/ops/shell/ousast.ps1 b/ops/shell/ousast.ps1 new file mode 100644 index 0000000..96fcc92 --- /dev/null +++ b/ops/shell/ousast.ps1 @@ -0,0 +1,52 @@ +# OpenUltraSAST as a local command. Dot-source this from your PowerShell profile: +# +# . C:\path\to\OpenUltraSAST\ops\shell\ousast.ps1 +# +# Then use it from any directory: +# +# ousast scan . +# ousast scan src\api +# +# Docker is an implementation detail. Your working directory is mounted READ-ONLY and findings are written +# to .\.ousast, the only writable mount. + +$script:OusastHome = if ($env:OUSAST_HOME) { $env:OUSAST_HOME } + else { Split-Path -Parent (Split-Path -Parent $PSScriptRoot) } + +function ousast { + $compose = Join-Path $script:OusastHome 'docker-compose.yml' + if (-not (Test-Path $compose)) { + Write-Error "ousast: cannot find $compose (set OUSAST_HOME to the checkout)"; return + } + if (-not (Get-Command docker -ErrorAction SilentlyContinue)) { + Write-Error "ousast: docker is not installed"; return + } + + $version = if ($env:OUSAST_VERSION) { $env:OUSAST_VERSION } else { 'dev' } + docker image inspect "openultrasast:$version" *> $null + if ($LASTEXITCODE -ne 0) { + Write-Host "ousast: building the analysis image once (~2 GB of engine; later runs are instant)" + Push-Location $script:OusastHome; docker compose build; Pop-Location + } + + # Rewrite paths under the current directory to their mounted equivalent; leave anything outside alone + # and say so, rather than silently rewriting it into a path the container cannot see. + $cwd = (Get-Location).Path + $mapped = foreach ($a in $args) { + if (Test-Path $a) { + $abs = (Resolve-Path $a).Path + if ($abs -eq $cwd) { '/target' } + elseif ($abs.StartsWith($cwd)) { '/target' + $abs.Substring($cwd.Length).Replace('\', '/') } + else { Write-Warning "ousast: $a is outside $cwd and is not mounted"; $a } + } else { $a } + } + + New-Item -ItemType Directory -Force -Path (Join-Path $cwd '.ousast') | Out-Null + $env:TARGET = $cwd + $env:OUSAST_REPORTS = Join-Path $cwd '.ousast' + if (-not $env:OUSAST_NETWORK) { $env:OUSAST_NETWORK = 'none' } + docker compose -f $compose run --rm ousast @mapped +} + +# The LLM judge needs network and a key; opt in explicitly. +function ousast-with-judge { $env:OUSAST_NETWORK = 'bridge'; ousast @args } diff --git a/ops/shell/ousast.sh b/ops/shell/ousast.sh new file mode 100644 index 0000000..0b1a1ed --- /dev/null +++ b/ops/shell/ousast.sh @@ -0,0 +1,81 @@ +# OpenUltraSAST as a local command. Source this from ~/.bashrc or ~/.zshrc: +# +# source /path/to/OpenUltraSAST/ops/shell/ousast.sh +# +# Then use it like any other linter, from any directory: +# +# ousast scan . +# ousast scan src/api +# ousast pairs --slice vibe-py +# +# Docker is an implementation detail on purpose. The engine is a JVM and ~2 GB of Joern, and a contributor +# evaluating this tool should not have to install either to try it. What they get instead is a command. +# +# The one thing worth knowing: your working directory is mounted READ-ONLY, and findings are written to +# ./.ousast, which is the only writable mount. A tool that reads your source to tell you about it has no +# business being able to change it. + +# Resolve this script's own location so the function works from anywhere, under bash or zsh. +if [ -n "${BASH_SOURCE[0]:-}" ]; then + _OUSAST_HOME="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +elif [ -n "${(%):-%N}" ] 2>/dev/null; then + _OUSAST_HOME="$(cd "$(dirname "${(%):-%N}")/../.." && pwd)" +fi +export OUSAST_HOME="${OUSAST_HOME:-$_OUSAST_HOME}" + +ousast() { + local compose="$OUSAST_HOME/docker-compose.yml" + if [ ! -f "$compose" ]; then + echo "ousast: cannot find $compose (set OUSAST_HOME to the checkout)" >&2 + return 1 + fi + + # A native install wins when it is there: it is faster, and someone who installed Joern deliberately + # should not be quietly routed through a container. + if [ "${OUSAST_PREFER_NATIVE:-1}" = "1" ] && command -v joern-parse >/dev/null 2>&1 \ + && [ -x "$OUSAST_HOME/.venv/bin/python" ]; then + ( cd "$OUSAST_HOME" && .venv/bin/python -m openultrasast.cli "$@" ) + return $? + fi + + if ! command -v docker >/dev/null 2>&1; then + echo "ousast: docker is not installed, and no local joern-parse was found" >&2 + return 1 + fi + + # Build once, silently, the first time. A first run that prints a wall of build output and then works is + # worse than one that pauses and explains itself. + if ! docker image inspect "openultrasast:${OUSAST_VERSION:-dev}" >/dev/null 2>&1; then + echo "ousast: building the analysis image once (this pulls ~2 GB of engine; later runs are instant)" >&2 + ( cd "$OUSAST_HOME" && docker compose build ) || return 1 + fi + + # Rewrite paths that live under the current directory to their mounted equivalent, so `ousast scan src/api` + # means what the user expects. Anything outside the mount is left alone and warned about rather than + # silently rewritten into a path the container cannot see. + local args=() a abs + for a in "$@"; do + if [ -e "$a" ]; then + abs="$(cd "$(dirname "$a")" 2>/dev/null && pwd)/$(basename "$a")" + case "$abs" in + "$PWD") args+=("/target") ;; + "$PWD"/*) args+=("/target${abs#"$PWD"}") ;; + *) echo "ousast: $a is outside $PWD and is not mounted; passing it through unchanged" >&2 + args+=("$a") ;; + esac + else + args+=("$a") + fi + done + + mkdir -p "$PWD/.ousast" + TARGET="$PWD" OUSAST_REPORTS="$PWD/.ousast" \ + OUSAST_NETWORK="${OUSAST_NETWORK:-none}" \ + docker compose -f "$compose" run --rm ousast "${args[@]}" +} + +# The LLM judge needs the network and a key; without them the model layer still entails and only the +# `suspicion` band goes unasked. This makes that opt-in explicit rather than a surprise. +ousast-with-judge() { + OUSAST_NETWORK=bridge ousast "$@" +}