Make any repo secure by default, keep it that way, and prove the controls fire.
- uses: MaXiMo000/carabiner@v0.1.14pip install carabiner-sec # installs the `carabiner` command
docker run --rm -v "$PWD:/repo:ro" ghcr.io/maximo000/carabiner:0.1.14 scan --allThe Marketplace display name is now Carabiner Repo Security — GitHub requires an action name to be unique across every action, user and organisation. The repository, the carabiner command and the carabiner-sec package are unchanged.
Calibrated against 60 public repositories
Tuned by scanning 60 real projects — tokio, grafana, vault, rails, next.js, home-assistant, airflow — across 13 languages, then re-checking every finding with an oracle written independently of the engines.
Default output is a median of 9 findings per repository; roughly 800 informational ones across the corpus are counted but not listed until --info. Hidden never means disappeared — the count always shows.
A version tag on an action is informational. A moving branch in someone else's repository is not. Classifying refs by shape rather than by a branch-name list also caught stable, nightly and cargo-hack. One unpinned reference is one finding, however many steps use it: tokio reaches dtolnay/rust-toolchain@stable 34 times in one workflow — one decision, not 34 lines.
tokio: 172 findings → 10. Corpus criticals: 31 → 2, and both survivors are real — a committed private key in grafana, and an unguarded pull_request_target + PR-head checkout in rollup.
A native Dockerfile engine
Trivy scans a built image — daemon, build, minutes. A Dockerfile is text, and the worst mistakes are visible before anything is built.
DOCK001 |
the final stage never drops root |
DOCK002 |
untagged or :latest base image |
DOCK003 |
a credential baked into ARG/ENV, readable with docker history |
DOCK004 |
a remote script piped straight into a shell at build time |
DOCK005 |
TLS verification disabled during the build |
54 real issues across 19 of the 60 repositories — after spot-checking killed 33 of the rule's first 36 findings. FROM builder is a stage reference, scratch is a keyword, ${BASE_IMAGE} is unknowable, and airflow embeds whole Python programs in BuildKit heredocs that were being parsed as instructions.
Findings that named an action or image were unreadable
The redactor treated /, . and - as token characters, so dtolnay/rust-toolchain@stable printed as dtol...hain@stable. A credential is a long unbroken run with no separators to split on — splitting on them keeps identifiers legible while still masking ghp_… and AKIA….
What it does
A ratchet. carabiner init accepts existing findings into a baseline, so CI goes green today and only new problems fail it. --expires 90 puts a deadline on accepted debt.
A drill. carabiner drill doesn't read config — it plants a private key and checks your hooks actually block it, then asks GitHub whether push protection is really on. A check that could not run never reports as passing.
One normalized model → SARIF into the PR's Security tab, deduplicated across engines, most-severe-wins.
52 tests, 193 checks, on Linux and Windows across Python 3.10 and 3.13, plus a Docker build, a wheel install into a clean venv, and carabiner scanning itself — every commit.