mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-10-16 19:59: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
+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
|
||||
Reference in New Issue
Block a user