Skip to content

Releases: MaXiMo000/carabiner

carabiner v0.2.1

Choose a tag to compare

@MaXiMo000 MaXiMo000 released this 23 Aug 07:17
pip install carabiner-sec

Fixed — --diff silently scanned nothing on Windows

Two bugs, one cause: OS-native path strings compared against forward-slash literals.

git reports .github/workflows/x.yml. pathlib on Windows produces .github\workflows\x.yml. The --diff filter compared the two, never matched, and so scanned no files at all — then reported a clean repository. A scan that reports "clean" because it examined nothing is exactly the failure this tool exists to complain about, and it shipped inside the tool.

The same cause routed every .circleci/config.yml to the Azure Pipelines parser, because the dispatch compared a native path string to the literal .circleci/config.yml.

Fixed at the boundary rather than at each comparison: Finding normalises its path to POSIX on construction, and every engine emits as_posix(). One missed comparison is a silent pass on one operating system, with no way to notice from the other.

Found by the Windows CI matrix, which was added two releases ago after Windows had been listed as "untested" for some time. It has now caught two real bugs — this one and a UnicodeDecodeError crash — in three days.

58 tests, 220 checks, Linux and Windows across Python 3.10 and 3.13.

carabiner v0.2.0

Choose a tag to compare

@MaXiMo000 MaXiMo000 released this 23 Aug 06:57
pip install carabiner-sec

Two new engines, a pre-commit path that is actually fast, and two drills that check controls people assume are on.

--diff — only what the commit touches

The first implementation filtered a full scan afterwards, which saves nothing: the work has already happened, and it measured slower than not using the flag. The engines now avoid the work instead, and secrets uses gitleaks' own --staged mode.

repository full tree --diff
next.js 3.92s 1.80s
grafana 4.25s 1.97s
home-assistant 6.21s 1.97s

kubernetes — native, no wrapper

K8S001 hostNetwork / hostPID / hostIPC — the container shares the node's namespaces
K8S002 a privileged container, which is root on the node in all but name
K8S003 allowPrivilegeEscalation, letting a setuid binary regain what the securityContext took away
K8S004 neither runAsNonRoot nor a non-zero runAsUser, so nothing prevents root
K8S005 a literal credential in env — committed in a manifest means committed in your history

K8S005's first two findings were both wrong. airflow's SECRET_NAME holds the name of a Kubernetes Secret and etcd's INITIAL_CLUSTER_TOKEN is a cluster identifier — both matched on the word alone. It now skips reference-shaped keys (…_NAME, …_PATH, …_ID) and requires the value to carry real entropy: a cluster name scores near 3.2, a credential above 4.

ci — Jenkins, CircleCI and Azure Pipelines

The last host gap. The same vulnerability in three dialects: text somebody else controls reaching a shell without ever becoming data.

  • JEN001 Groovy interpolates ${params.X} inside double quotes before the shell ever runs, so the value arrives as code. Single quotes do not, which is why the quote style is the finding.
  • JEN002 a literal credential assigned in a pipeline
  • CIR001 an orb pinned to @volatile, which always resolves to the newest publish
  • CIR002 a pipeline parameter substituted into a run step
  • AZP001 a branch name or commit message substituted into a script step

Deduplicated by resolved path: macOS and Windows are case-insensitive, so Jenkinsfile and jenkinsfile matched as two files and every finding was reported twice.

Jenkinsfiles are Groovy, so that engine is regex-level and shallower than the parsed GitHub and GitLab ones — stated plainly rather than implied.

Two more drills

  • DRILL017 — are Dependabot alerts actually enabled? A dependabot.yml in the repository does not switch them on.
  • DRILL018 — has any SARIF ever arrived? A rejected upload does not fail the job that produced it, so a green pipeline and an empty Security tab look identical from the outside.

57 tests, 218 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.

carabiner v0.1.14

Choose a tag to compare

@MaXiMo000 MaXiMo000 released this 22 Aug 21:24

Make any repo secure by default, keep it that way, and prove the controls fire.

- uses: MaXiMo000/carabiner@v0.1.14
pip install carabiner-sec        # installs the `carabiner` command
docker run --rm -v "$PWD:/repo:ro" ghcr.io/maximo000/carabiner:0.1.14 scan --all

The 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.

carabiner v0.1.13

Choose a tag to compare

@MaXiMo000 MaXiMo000 released this 22 Aug 21:22

Make any repo secure by default, keep it that way, and prove the controls fire.

- uses: MaXiMo000/carabiner@v0.1.13
pip install carabiner-sec        # installs the `carabiner` command
docker run --rm -v "$PWD:/repo:ro" ghcr.io/maximo000/carabiner:0.1.13 scan --all

Calibrated against 60 public repositories

Tuned by scanning 60 real projects — tokio, grafana, vault, rails, next.js, home-assistant, airflow and more — across 13 languages, then re-checking every finding with an oracle written independently of the engines.

The default output is now a median of 9 findings per repository. Roughly 800 informational ones across the corpus are counted but not listed until you pass --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 — that is what can change tonight without a diff on your side. Classifying refs by shape rather than by a list of branch names also caught stable, nightly and cargo-hack. And one unpinned reference is one finding, however many steps use it: tokio reaches dtolnay/rust-toolchain@stable 34 times in a single workflow, which is one decision, not 34 lines.

tokio went from 172 findings to 10. Corpus criticals went from 31 to 2 — and both survivors are real: a committed private key in grafana, and an unguarded pull_request_target + PR-head checkout in rollup.

New: a native Dockerfile engine

Trivy scans a built image — daemon, build, minutes. A Dockerfile is text, and the mistakes that matter most 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 — but only 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 entire Python programs in BuildKit heredocs that were being parsed as instructions.

Fixed: 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… tokens.

What it does

A ratchet. carabiner init accepts your 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.

carabiner v0.1.11

Choose a tag to compare

@MaXiMo000 MaXiMo000 released this 22 Aug 21:01
pip install carabiner-sec

Consolidates v0.1.6–v0.1.11. This run was driven by scanning 60 public repositories (4.0 GB across Python, JS/TS, Go, Rust, Java, Kotlin, Ruby, PHP, C, C++, C#, Elixir, Swift and shell) and then re-checking every finding with an oracle written independently of the engines.

Precision

Critical findings across the corpus went from 31 to 2 — and both survivors are real: a committed -----BEGIN PRIVATE KEY----- in grafana, and an unguarded pull_request_target + PR-head checkout in rollup.

Five false-positive classes were found and fixed. Four were the same mistake wearing different clothes — judging a file by its name instead of reading it:

What Evidence
.env flagged by filename grafana commits 11 .env files holding mysql_version=8.0.32
.env.example treated as real strapi ships JWT_SECRET=tobemodified — that is documentation
"AUTH" matched inside "AUTHOR" LINEAR_CMS_STATUS_WAITING_ON_AUTHOR=<uuid> read as a credential
test-fixtures unmatched vault's 14 test keys were critical because the directory name is hyphenated
.npmrc assumed to hold a token every one in the corpus contained ignore-scripts=true

CI002 was separately wrong two times in three: pull_request.base.ref is the PR's target branch, and a GitHub username cannot carry a shell metacharacter. It now names the fields an outsider can actually put text into.

A guarded pull_request_target (discourse gates on the author's login, which is not spoofable) now reports high and says the guard exists, rather than crying open-door over a closed one.

Correctness

  • Windows crash fixed — text I/O used the locale encoding, so one em dash in a workflow file killed the scan with UnicodeDecodeError. Windows is now in the test matrix.
  • Dependencies in subprojects were silently skipped. backend/ and frontend/ lockfiles were invisible; the scan looked clean rather than unchecked.
  • --offline is now real and enforced by a test that blocks socket creation.
  • Packaging — the distribution is carabiner-sec; the command is still carabiner.

Performance

deps moved behind --all: its OSV lookups cost 0.7–9.3s while every other engine stayed under 1.7s. Worst fast path across the corpus went 10.2s → 2.4s, median 0.47s.

48 tests, 161 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.

carabiner v0.1.5

Choose a tag to compare

@MaXiMo000 MaXiMo000 released this 22 Aug 20:10

Dependencies in subprojects were silently skipped.

Found on the first repository carabiner was pointed at that I had not written myself: backend/ and frontend/ each carried a package-lock.json, the deps engine looked only at the top level, found nothing, and therefore said nothing. The scan looked like a repo with no dependency problems rather than one where dependencies were never checked.

The root cause was carabiner's own gate — osv-scanner is invoked with -r and recurses perfectly well. Now a depth-bounded walk that prunes node_modules and friends: 0.017s on that repo against a 2s budget, so the performance concern that justified "top level only" was unfounded.

On that repository the fix surfaced 186 advisories that had been invisible, including a CRITICAL and 90 HIGH.

carabiner v0.1.4

Choose a tag to compare

@MaXiMo000 MaXiMo000 released this 22 Aug 19:46
pip install carabiner-sec

Fixed — a crash on Windows

Path.read_text() uses the locale encoding on Windows, not UTF-8. One em dash, smart quote or emoji anywhere in a scanned file took the whole scan down with UnicodeDecodeError — a crash for most Windows users, not an edge case.

Files read from your repo now use UTF-8 with errors="replace", because a stray byte shouldn't end a scan. Files carabiner writes use strict UTF-8. Pinned by an AST-based lint over every text read and write in the codebase.

This was found by adding Windows to the CI matrix, which had been listed as untested for several releases. It paid for itself on the first run.

Added

  • Windows is now tested — Linux and Windows, Python 3.10 and 3.13.
  • --offline is real. It previously only reached the drills while deps still called out through osv-scanner, and SECURITY.md claimed otherwise. Engines now declare whether they need the network and are skipped entirely, enforced by a test that blocks socket creation and asserts a full scan still completes.
  • PyPI project metadata — the page was blank. README, classifiers, keywords, and PEP 639 licence metadata.

Also

The README had silently gone stale — most edits to it had no-opped, leaving it claiming "Phase 0, native engines only" long after five engines, the drill, SARIF, Docker and PyPI shipped. Rewritten, and now covered by tests asserting the docs pin the shipped version, recommend no mutable ref (@main is exactly what CI003 reports), and mention every registered engine.

Full diff: v0.1.2...v0.1.4

carabiner v0.1.2

Choose a tag to compare

@MaXiMo000 MaXiMo000 released this 22 Aug 19:10

First release published to PyPI.

pip install carabiner-sec        # installs the `carabiner` command

The PyPI name carabiner was already taken by an unrelated package, so the distribution is carabiner-sec. The command you type is unchanged.

Fixed since v0.1.0 — adding the landing page gave the repo a second top-level directory, and setuptools correctly refused to guess which one was the package. pip install . had worked locally right up until site/ existed. Packages are now declared explicitly, and CI builds the distribution and installs the wheel into a clean venv on every commit — a break that only appears at release is one that appears in front of users.

Published via PyPI trusted publishing over OIDC. No API token exists in this repository.

carabiner v0.1.0

Choose a tag to compare

@MaXiMo000 MaXiMo000 released this 22 Aug 18:16

First release. Four engines, the ratchet, the drill, and three ways to install it.

What it does

  • The ratchetcarabiner init accepts every existing finding into a baseline, so CI goes green immediately and only new findings fail it. Adoptable in a ten-year-old repo without a cleanup sprint. Findings are fingerprinted on content, never line numbers, so reformatting a file doesn't resurrect the backlog. --expires 90 puts a deadline on accepted debt.
  • The drillcarabiner drill doesn't read config. It plants a private key and runs your installed pre-commit hooks against it, then asks GitHub whether push protection is on and whether your default branch actually requires a status check. A drill that couldn't run never reports as passing.
  • One normalized model — SARIF into the PR Security tab, deduplicated across engines, most-severe-wins.

Engines

Engine Needs
ci — GitHub Actions: pull_request_target + PR-head checkout, github.event script injection, unpinned actions, token blast radius, self-hosted runners nothing
ci — GitLab CI: merge-request-title script injection, unpinned remote include:, mutable image tags nothing
repo.gitignore gaps, committed key material, credentials in git remotes nothing
secrets gitleaks
deps — PyPI, npm, Go, Maven, crates.io… ids normalized to CVE osv-scanner

Install

- uses: MaXiMo000/carabiner@v0.1.0
docker run --rm -v "$PWD:/repo:ro" ghcr.io/maximo000/carabiner:v0.1.0 scan --all

The image bundles both scanners, pins its base by digest, checksum-verifies every binary it downloads, runs as a non-root user, and ships with a build-provenance attestation. linux/amd64 only for now.

Known limits — Jenkins, CircleCI and Bitbucket get every engine except ci. Windows is untested. Only top-level dependency manifests are read.

Site: https://maximo000.github.io/carabiner/