From 8037cb7908f0ec665f690de09e8b8ff228118fbe Mon Sep 17 00:00:00 2001 From: Flo Date: Tue, 31 Mar 2026 14:03:29 +0200 Subject: [PATCH] feat: v0.2.0 expanded hardening Add gitleaks pre-commit hook, global gitignore, plaintext credential detection, SSH key hygiene audit, 8 new git config settings, and safe.directory wildcard detection. Fix ssh-keygen macOS compatibility, FIDO2 detection via ioreg, and interactive test isolation. Implements docs/specs/2026-03-31-v0.2.0-expanded-hardening.md Co-Authored-By: Claude --- CHANGELOG.md | 24 +- README.md | 26 +- docs/research/Claude Opus 4.6 report.md | 483 ++++++++++++++++++ docs/research/Gemini 3.1 Pro report.md | 368 +++++++++++++ .../2026-03-31-v0.2.0-expanded-hardening.md | 330 ++++++++++++ git-harden.sh | 473 +++++++++++++++-- test/git-harden.bats | 330 ++++++++++++ test/interactive/helpers.sh | 20 +- test/interactive/test-full-accept.sh | 8 +- test/interactive/test-signing-generate.sh | 10 +- test/interactive/test-signing-skip.sh | 12 +- 11 files changed, 2019 insertions(+), 65 deletions(-) create mode 100644 docs/research/Claude Opus 4.6 report.md create mode 100644 docs/research/Gemini 3.1 Pro report.md create mode 100644 docs/specs/2026-03-31-v0.2.0-expanded-hardening.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 057e294..ca3012a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,27 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## [0.1.0] - 2026-03-31 +## [0.2.0] - 2026-03-31 + +### Added +- Gitleaks pre-commit hook installation — creates `~/.config/git/hooks/pre-commit` with `SKIP_GITLEAKS` bypass +- Global gitignore creation (`~/.config/git/ignore`) with security patterns (`.env`, `*.pem`, `*.key`, credentials, Terraform state) +- Audit of existing global gitignore for missing security patterns +- 8 new git config settings: `user.useConfigOnly`, `protocol.version=2`, `transfer.bundleURI=false`, `init.defaultBranch=main`, `core.symlinks=false` (interactive-only), `fetch.prune=true`, `gc.reflogExpire=180.days`, `gc.reflogExpireUnreachable=90.days` +- Combined signing enablement into single prompt (replaces 3 individual prompts) +- 26 new BATS tests (90 total) + +### Security +- SSH key hygiene audit — scans `~/.ssh/*.pub` and `IdentityFile` entries, warns about DSA/ECDSA/weak RSA keys +- Plaintext credential file detection — warns about `~/.git-credentials`, `~/.netrc`, `~/.npmrc` (auth tokens), `~/.pypirc` (passwords) +- `safe.directory = *` wildcard detection and removal (CVE-2022-24765) + +### Fixed +- `ssh-keygen` calls fail on macOS with `--` end-of-options separator (removed) +- Interactive tests fail on macOS due to tmux resetting `HOME` in login shells +- Interactive tests race condition with tmux session cleanup between tests + +## [0.1.0] - 2026-03-30 ### Added - Interactive shell script that audits and hardens global git config @@ -32,5 +52,3 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - SSH config value parsing handles inline comments and quoted paths - Version comparison uses base-10 arithmetic to prevent octal interpretation - Temp file cleanup trap in SSH config updates -- `--` separator before path arguments in `ssh-keygen` calls -- Removed unused exported `SIGNING_KEY_PATH` variable diff --git a/README.md b/README.md index 4033e8f..2ded5d2 100644 --- a/README.md +++ b/README.md @@ -38,16 +38,23 @@ The script runs in two phases: | Category | What it does | |---|---| -| **Object integrity** | Validates all objects on fetch/push/receive (`transfer.fsckObjects`, etc.) | -| **Protocol restrictions** | Default-deny policy: only HTTPS and SSH allowed. Blocks `git://` (unencrypted) and `ext://` (arbitrary command execution) | -| **Filesystem protection** | Enables `core.protectNTFS`, `core.protectHFS`, disables `core.fsmonitor` | +| **Identity** | `user.useConfigOnly=true` — prevents commits without explicit identity | +| **Object integrity** | `fsckObjects` on transfer/fetch/receive, `transfer.bundleURI=false`, `fetch.prune=true` | +| **Protocol restrictions** | Default-deny policy: only HTTPS and SSH. Blocks `git://` and `ext://`. Forces `protocol.version=2` | +| **Filesystem protection** | `core.protectNTFS`, `core.protectHFS`, `core.fsmonitor=false`, `core.symlinks=false` (interactive-only) | | **Hook control** | Redirects `core.hooksPath` to `~/.config/git/hooks` so repo-local hooks can't execute | -| **Repository safety** | `safe.bareRepository=explicit`, `submodule.recurse=false` | -| **Pull/merge hardening** | `pull.ff=only`, `merge.ff=only` — refuses non-fast-forward merges, surfacing rewritten history | +| **Pre-commit hook** | Installs gitleaks secret scanner as global pre-commit hook (with `SKIP_GITLEAKS` bypass) | +| **Repository safety** | `safe.bareRepository=explicit`, `submodule.recurse=false`, detects/removes `safe.directory=*` wildcard | +| **Pull/merge hardening** | `pull.ff=only`, `merge.ff=only` — refuses non-fast-forward merges | | **Transport security** | Rewrites `http://` to `https://`, enforces `http.sslVerify=true` | | **Credential storage** | Platform-detected secure helper (`osxkeychain` on macOS, `libsecret` on Linux). Warns if using plaintext `store` | +| **Credential hygiene** | Warns about plaintext `~/.git-credentials`, `~/.netrc`, `~/.npmrc` (tokens), `~/.pypirc` (passwords) | +| **Global gitignore** | Creates `~/.config/git/ignore` with patterns for secrets, credentials, and OS/IDE artifacts | +| **Defaults** | `init.defaultBranch=main` | +| **Forensic readiness** | Extended reflog retention (`gc.reflogExpire=180.days`, `gc.reflogExpireUnreachable=90.days`) | | **Commit signing** | SSH-based signing with interactive key setup wizard (software or FIDO2 hardware key) | | **SSH hardening** | `StrictHostKeyChecking=accept-new`, `HashKnownHosts=yes`, `IdentitiesOnly=yes`, modern algorithm restrictions | +| **SSH key hygiene** | Audits `~/.ssh/*.pub` for weak key types (DSA, ECDSA, short RSA) | | **Visibility** | `log.showSignature=true` | A config backup is saved to `~/.config/git/pre-harden-backup-.txt` before any changes. @@ -95,6 +102,7 @@ Options: - Bash 3.2+ (compatible with macOS default bash) Optional: +- `gitleaks` for pre-commit secret scanning (hook is installed regardless; scans run only if gitleaks is on `$PATH`) - `ykman` or `fido2-token` for FIDO2 hardware key detection ## Threat Model @@ -106,9 +114,11 @@ Optional: - **Protocol downgrade** — blocks plaintext `git://` and dangerous `ext://` protocol - **Hook-based RCE** — redirects hook execution away from repo-local `.git/hooks/` - **Submodule attacks** — disables auto-recursion; submodules must be explicitly initialized -- **Credential theft** — ensures secure credential storage, warns about plaintext `store` +- **Credential theft** — ensures secure credential storage, warns about plaintext `store`, detects leaked credentials in `~/.git-credentials`, `~/.netrc`, `~/.npmrc`, `~/.pypirc` +- **Secret leakage** — gitleaks pre-commit hook blocks commits containing secrets before they enter git history - **Commit impersonation** — SSH signing proves key possession (anyone can fake `user.name`/`user.email`) -- **Filesystem tricks** — blocks NTFS/HFS+ path manipulation attacks +- **Filesystem tricks** — blocks NTFS/HFS+/symlink path manipulation attacks +- **Weak SSH keys** — audits and warns about DSA, ECDSA, and short RSA keys ### What this does NOT protect against @@ -132,7 +142,7 @@ The script prints (but does not apply) server/org-level recommendations: ## Running Tests ```bash -# Run the BATS test suite (64 tests) +# Run the BATS test suite (90 tests) ./test/run.sh # Requires bats-core submodules — init them if needed diff --git a/docs/research/Claude Opus 4.6 report.md b/docs/research/Claude Opus 4.6 report.md new file mode 100644 index 0000000..a9c17b8 --- /dev/null +++ b/docs/research/Claude Opus 4.6 report.md @@ -0,0 +1,483 @@ +# Git Security Hardening: A Practitioner's Reference + +**The single most impactful thing an organization can do to harden its git security posture is enable push-time secret scanning.** GitGuardian's 2026 State of Secrets Sprawl report found **29 million new hardcoded secrets** on public GitHub in 2025 alone — a 34% year-over-year increase — with 64% of secrets from 2022 still unrevoked. Secret exposure remains the highest-likelihood, highest-impact attack surface because it requires zero sophistication to exploit: attackers scan public commits in real time, with a median time-to-discovery of **20 seconds** (Meli et al., NDSS 2019). Combined with branch protection enforcement, commit signing, and least-privilege token management, organizations can eliminate the most common git-related breach vectors with moderate effort. + +This report covers the full git attack surface — from developer workstations through hosted platforms to CI/CD integration points — with platform-specific guidance for GitHub, GitLab, and Azure DevOps. Each section includes a threat model, numbered hardening checklist, real-world incident motivation, and residual risk assessment. + +--- + +## Executive summary: ten highest-impact hardening measures + +Ranked by risk reduction per unit of implementation effort for a typical 10–50 developer organization: + +1. **Enable push-time secret scanning** (GitHub Secret Protection or GitLab Ultimate push protection). Blocks the most frequently exploited vulnerability class before it enters the repository. ~1 hour to enable org-wide. +2. **Require pull request reviews on default and release branches** with dismissal of stale approvals. Prevents direct pushes of malicious or vulnerable code. ~30 minutes per repository, automatable via rulesets. +3. **Replace classic PATs with fine-grained, short-lived tokens** (GitHub fine-grained PATs, GitLab project tokens, or GitHub Apps). Eliminates the "keys to the kingdom" single-token failure mode that enabled the tj-actions and Trivy compromises. ~1 day for audit and migration. +4. **Enforce 2FA/MFA for all organization members.** Prevents account takeover — the root cause of the Gentoo GitHub compromise. ~1 hour to enable; budget 2 weeks for member compliance. +5. **Install a pre-commit hook stack** (gitleaks + framework) on all developer machines. Catches secrets before they enter git history, where removal is costly. ~2 hours via `core.hooksPath`. +6. **Pin GitHub Actions to full commit SHAs**, not version tags. Prevents supply-chain injection via mutable tags, as exploited in the tj-actions/changed-files and Trivy incidents. ~1 day for audit and update. +7. **Enable SSH commit signing with ed25519 keys.** Prevents commit author impersonation — the attack vector in the PHP git server compromise. ~1 hour per developer. +8. **Deploy a hardened `.gitconfig` template** org-wide (`fsckObjects`, `safe.bareRepository = explicit`, protocol restrictions). Blocks multiple client-side attack classes including CVE-2024-32002. ~30 minutes. +9. **Stream audit logs to a SIEM** (or at minimum, enable and review them weekly). Provides detection of privilege escalation, branch protection tampering, and anomalous clone activity. ~4 hours for initial setup. +10. **Restrict AI coding agent permissions** — enforce least-privilege tokens, prevent `--no-verify` bypasses, and require PR review for all AI-generated commits. Addresses the fastest-growing secret exposure vector (AI-assisted commits leak secrets at **2× the baseline rate**). + +For organizations that can implement only five changes this quarter, start with items 1, 2, 3, 4, and 5. + +--- + +## Table of contents + +1. [Secret exposure](#1-secret-exposure) +2. [Authentication and access control](#2-authentication-and-access-control) +3. [Commit integrity](#3-commit-integrity) +4. [Branch protection and code review enforcement](#4-branch-protection-and-code-review-enforcement) +5. [Supply chain attacks via git](#5-supply-chain-attacks-via-git) +6. [Git hosting platform hardening](#6-git-hosting-platform-hardening) +7. [Developer workstation git security](#7-developer-workstation-git-security) +8. [Audit, monitoring, and incident response](#8-audit-monitoring-and-incident-response) +9. [Platform security feature comparison](#9-github-vs-gitlab-vs-azure-devops-security-feature-comparison) +10. [Appendix A: Minimal `.gitconfig` hardening template](#appendix-a-minimal-gitconfig-hardening-template) +11. [Appendix B: Pre-commit hook stack recommendation](#appendix-b-pre-commit-hook-stack-recommendation) + +--- + +## 1. Secret exposure + +### Threat model + +**Who:** Any attacker scanning public repositories, or an insider with read access to private repos. **What:** Credentials, API keys, private keys, database connection strings committed to git history. **How:** Automated scanning of commits in real time (bots monitor the GitHub Events API), manual inspection after a breach, or scraping of repository history. + +The scale of this problem is staggering. The landmark NDSS 2019 study by Meli et al. ("How Bad Can It Git?") found over **100,000 repositories** with leaked secrets, with a median of **1,793 unique new keys appearing per day** across public GitHub. Of these, **89% were genuinely sensitive** — not test keys. GitGuardian's 2026 report shows the problem is accelerating: **29 million new secrets** detected in 2025, and repositories using AI coding assistants exhibit a **6.4% secret leakage rate** versus a 1.5% baseline. + +The Uber 2016 breach is the canonical cautionary tale: attackers found **hardcoded AWS access keys** in a private GitHub repository (accessed via credential-stuffed passwords on accounts without MFA). Those keys unlocked an S3 bucket containing 57 million user records. The result: a **$148 million FTC fine** and the criminal conviction of Uber's CISO for concealing the breach. + +### Hardening checklist + +**1. Enable push-time secret scanning on the hosting platform.** GitHub Secret Protection ($19/committer/month, now available on Team plans) blocks pushes containing detected secret patterns before they enter the repository. GitLab Ultimate provides equivalent push protection via server-side pre-receive hooks. Azure DevOps offers GitHub Advanced Security for Azure DevOps with similar capabilities. Push-time blocking is dramatically more effective than post-commit alerting — GitGuardian data shows **70% of secrets leaked in 2022 remained active** through 2025, indicating that alert-only approaches fail due to slow remediation. + +**2. Deploy a pre-commit secret scanning tool** on all developer workstations (see Appendix B for detailed tool selection). This catches secrets before they reach the server, complementing platform-side scanning. + +**3. Maintain comprehensive `.gitignore` patterns.** Every repository should exclude `.env`, `*.pem`, `*.key`, `*.p12`, `*.tfstate`, `credentials.json`, `service-account*.json`, `.npmrc`, `.pypirc`, and similar files. Use a global gitignore (`core.excludesfile`) for personal patterns plus repository-level `.gitignore` for project-specific ones. + +**4. Understand and address the persistence problem.** Running `git rm secret.key && git commit` does **not** remove the secret from history. Git stores content as immutable blob objects; the original blob persists in packfiles and is cloned by every downstream user. **The primary remediation is always to rotate the secret immediately.** History rewriting is secondary cleanup. Use `git-filter-repo` (recommended by the Git project as the replacement for the deprecated `git filter-branch`) or BFG Repo-Cleaner. After rewriting, run `git reflog expire --expire=now --all && git gc --prune=now --aggressive`, force-push, and contact platform support to purge cached objects. Note that forks retain the pre-rewrite history, and all developers must delete and re-clone. + +**5. Configure custom secret patterns** for organization-specific credential formats (internal API keys, database connection strings) beyond the default detection patterns provided by platforms. + +### Residual risk + +Push-time scanning catches only **known secret patterns**. Generic passwords, custom token formats, and secrets embedded in binary files evade pattern-based detection. The 2026 GitGuardian report notes that **58% of leaked credentials are "generic secrets"** that bypass standard detection. Defense-in-depth (pre-commit hooks + push protection + periodic full-history scans with TruffleHog's active verification) reduces but does not eliminate this residual risk. The operational cost is modest: pre-commit hooks add ~1–3 seconds to each commit, and occasional false positives require developer time to triage. + +--- + +## 2. Authentication and access control + +### Threat model + +**Who:** External attackers via credential theft, phishing, or token leakage; insiders with excessive permissions. **What:** Unauthorized access to repositories, code modification, secret exfiltration. **How:** Compromised PATs, stolen SSH keys, OAuth token theft, or account takeover via password reuse. + +The April 2022 Heroku/Travis CI OAuth token compromise demonstrated the blast radius of over-privileged tokens: stolen OAuth tokens from two integrators provided access to private repositories of dozens of organizations, including GitHub's own npm infrastructure. More recently, the March 2025 tj-actions/changed-files compromise (CVE-2025-30066) stemmed from a single compromised PAT belonging to a bot account — that one token affected **23,000+ downstream repositories**. The March 2026 Trivy supply-chain attack (CVE-2026-28353, CVSS 10.0) traced back to a single org-scoped PAT (`ORG_REPO_TOKEN`) used across 33 workflows; incomplete rotation after the first breach enabled a second, more devastating attack. + +### Hardening checklist + +**1. Use ed25519 SSH keys exclusively.** Ed25519 provides equivalent security to RSA-4096 with much smaller keys (68 vs. 544 characters), faster operations, and no parameter-selection pitfalls. Generate with `ssh-keygen -t ed25519 -C "user@company.com"`. GitHub blocks legacy RSA/SHA-1 signatures. For maximum security, use FIDO2 hardware-backed keys: `ssh-keygen -t ed25519-sk -O resident -O verify-required`. + +**2. Replace classic PATs with fine-grained, scoped tokens.** GitHub classic PATs (`ghp_` prefix) with `repo` scope grant read/write access to **every repository the user can access** — a textbook violation of least privilege. GitHub fine-grained PATs (`github_pat_` prefix) scope to specific repositories with granular permissions and mandatory expiration. GitLab project/group access tokens provide similar scoping. Set maximum PAT lifetimes via org policy: 90 days for CI/CD, 30 days for one-off tasks. + +**3. Prefer GitHub Apps over PATs for automation.** GitHub Apps generate **short-lived installation tokens** (1-hour expiry) with fine-grained, repository-scoped permissions — not tied to any human account. They survive employee departures without credential rotation and provide auditable "on behalf of" action trails. This is the single most effective control against the "over-privileged bot token" failure mode. + +**4. Enforce SAML SSO and audit legacy credentials.** On GitHub Enterprise Cloud, PATs and SSH keys must be separately authorized for SSO after creation. Critical gap: on GitLab, project/group access tokens and deploy keys **bypass SSO enforcement entirely**. On Azure DevOps, PATs bypass device compliance and MFA requirements — only IP-fencing policies apply to non-interactive flows. + +**5. Deploy SSH Certificate Authorities** (GitHub Enterprise Cloud). Certificates expire automatically (e.g., daily), eliminating key rotation as a manual process. CAs uploaded after March 2024 require certificate expiration dates. + +**6. Implement IP allowlisting where feasible** (GitHub Enterprise Cloud, GitLab self-managed, Azure DevOps via Entra Conditional Access). Practical limitation: dynamic IPs for remote workers require VPN routing, and GitHub-hosted Actions runners have dynamic IPs — requiring self-hosted or larger runners with static IPs. + +### Residual risk + +SSO enforcement does not protect against all token types on all platforms. IP allowlisting is operationally expensive with distributed teams. Hardware security keys (FIDO2) provide the strongest authentication but introduce device-dependency risk. The operational cost of migrating from classic PATs to fine-grained PATs or GitHub Apps is moderate — budget 1–2 days for audit and migration per team. + +--- + +## 3. Commit integrity + +### Threat model + +**Who:** Attackers with push access (via compromised credentials or server compromise) impersonating trusted developers. **What:** Forged commits attributed to maintainers, containing backdoors or malicious changes. **How:** Git allows arbitrary `user.name` and `user.email` configuration — without signing, anyone can commit as anyone. + +The March 2021 PHP git server compromise is the definitive case study. Attackers pushed two commits to the official `php-src` repository on the self-hosted `git.php.net` server: one attributed to PHP creator Rasmus Lerdorf, another to core maintainer Nikita Popov. Both inserted a backdoor that would execute arbitrary PHP code via a specially crafted HTTP header. The commits had `Signed-off-by` trailers — but those are plain text, not cryptographic signatures. The PHP project subsequently migrated to GitHub and mandated 2FA. + +### Hardening checklist + +**1. Enable SSH commit signing org-wide** (recommended over GPG for simplicity). SSH signing, available since Git 2.34, uses keys developers already manage — no GPG keyring overhead. Configuration: + +``` +git config --global gpg.format ssh +git config --global user.signingkey ~/.ssh/id_ed25519.pub +git config --global commit.gpgsign true +git config --global tag.gpgsign true +``` + +Maintain an `allowed_signers` file for local verification: `git config --global gpg.ssh.allowedSignersFile ~/.config/git/allowed_signers`. + +**2. Enable GitHub Vigilant Mode** (Settings → SSH and GPG keys → "Flag unsigned commits as unverified"). Without this, unsigned commits show no badge at all — making unsigned and forged commits visually indistinguishable from legitimate ones. + +**3. Require signed commits via branch protection** on all default and release branches. GitHub supports this in both classic branch protection and rulesets. GitLab supports it via push rules. **Azure DevOps has no native commit signing verification** — signed commits are accepted but not validated or badged. Third-party pipeline decorators exist but are not equivalent. + +**4. Understand what signing does and does not guarantee.** Signing proves **key possession at commit time** — it defeats impersonation and detects tampering. It does **not** prove the legitimate key owner was at the keyboard (a compromised machine with access to `ssh-agent` can produce valid signatures), and it does not prevent intentional malicious commits by a valid signer. Signing is an accountability control, not an authorization control. + +**5. Monitor the SHA-1 to SHA-256 transition.** Git has used hardened SHA-1 (the `sha1collisiondetection` library) since Git 2.13.0, which detects known collision attack patterns with negligible false positive rates (< 2⁻⁹⁰). The SHA-1 chosen-prefix collision cost has fallen to approximately **$45,000** (Leurent & Peyrin, USENIX Security 2020, "SHA-1 is a Shambles"), and continues to decline with GPU advances. Git 3.0 (targeted for late 2026) will default new repositories to SHA-256, but **GitHub does not yet support SHA-256 repositories**, creating an ecosystem blocker. Practical risk today: low for most organizations due to the hardened SHA-1, but plan for migration. + +### Residual risk + +GitHub's squash-and-merge and rebase-and-merge operations create new unsigned commits, breaking signing chains. Sigstore/Gitsign offers keyless signing via OIDC identity, eliminating key management entirely, but GitHub does not yet display "Verified" badges for Gitsign signatures. For signing release artifacts (tarballs, binaries), signify or minisign are lightweight alternatives to GPG. + +--- + +## 4. Branch protection and code review enforcement + +### Threat model + +**Who:** Malicious insiders, compromised accounts, or external attackers with any write access. **What:** Direct pushes of malicious code to production branches bypassing review. **How:** Pushing directly to unprotected branches, self-approving PRs, exploiting bypass vectors in branch protection configuration. + +### Hardening checklist + +**1. Require pull requests with at minimum one approving review** on default and release branches across all repositories. Use GitHub rulesets (preferred over classic branch protection — they aggregate restrictively, apply across fork networks, and support org-wide governance) or GitLab protected branches with "Allowed to push: No one." + +**2. Enable "Dismiss stale approvals when new commits are pushed"** on all platforms. Without this, a developer can get approval, then push additional malicious commits and merge without re-review. + +**3. Enable "Require approval of the most recent reviewable push"** (GitHub) or equivalent. This prevents a reviewer from pushing commits to a PR branch and then approving their own additions — a documented bypass vector identified by Legit Security that GitHub considers "working as expected." + +**4. Check "Do not allow bypassing the above settings"** (GitHub) or restrict unprotect permissions (GitLab). By default, GitHub repository admins can bypass all branch protections. This single checkbox is the difference between "branch protection exists" and "branch protection is enforced." + +**5. Use CODEOWNERS for security-critical paths** (authentication modules, cryptography, CI/CD configurations, the CODEOWNERS file itself). Protect the CODEOWNERS file with a self-referencing entry: `/.github/CODEOWNERS @security-team`. Validate that all listed code owners are active members with write permissions — invalid or departed users cause CODEOWNERS enforcement to fail silently. + +**6. Require status checks before merging** (CI tests, SAST scans, secret scanning). Block merge unless all checks pass and branches are up to date with the target. + +**7. Restrict force-push and branch deletion** on protected branches (default on GitHub and GitLab for protected branches, but verify explicitly). + +### The "rubber stamp" problem + +Branch protection is only as strong as the review process behind it. Research by Edmundson et al. (2013) found that **none of 30 developers** reviewing a web application found all seven known vulnerabilities, and more experience did not correlate with better detection. A 2024 study of OpenSSL and PHP code reviews found that even when security concerns were raised, **18–20% went unfixed** due to disagreements. Code review becomes security theater when PRs are approved in under two minutes without comments, when reviewers lack security training, or when PR sizes exceed 400 lines. + +Minimum viable security review: keep PRs under 400 lines, run automated SAST and secret scanning before human review, require conversation resolution before merge, and designate security-trained reviewers as CODEOWNERS for sensitive paths. + +### Bypass vectors to monitor + +Admin override (without "Do not allow bypassing"), GitHub Actions self-approval (a bot can use `GITHUB_TOKEN` to approve PRs — disable via org settings), PAT-based PR creation and approval (if a `repo`-scoped PAT exists in Actions secrets), and CODEOWNERS misconfiguration (invalid users, unprotected CODEOWNERS file, or the "Require review from Code Owners" toggle not enabled). + +--- + +## 5. Supply chain attacks via git + +### Threat model + +**Who:** Nation-state actors (xz-utils pattern), opportunistic attackers (pwn requests), or automated bots. **What:** Injecting malicious code into software supply chains via git-hosted repositories, build systems, or CI/CD pipelines. **How:** Social engineering of maintainers, exploitation of CI workflow misconfigurations, fork-based object injection, or dependency confusion. + +The **xz-utils backdoor** (CVE-2024-3094, CVSS 10.0) represents the most sophisticated git-related supply chain attack to date. An attacker using the identity "Jia Tan" spent **over two years** building trust in the xz-utils project through legitimate contributions, while sockpuppet accounts ("Jigar Kumar," "Dennis Ens") pressured the burned-out sole maintainer to grant commit access. The backdoor was injected via the build system (M4 macros in the release tarball, not visible in the git source), targeting SSH on Debian/Fedora x86_64 systems. Discovery was accidental: Microsoft engineer Andres Freund noticed SSH logins consuming 500ms instead of 100ms. + +The **Codecov breach** (January–April 2021) demonstrated a different vector: attackers extracted a GCS credential from a Docker image layer, modified the Codecov bash uploader script to exfiltrate CI environment variables (including git credentials and tokens) from approximately **29,000 enterprise customers'** CI runners. A customer discovered the compromise by comparing the downloaded script's SHA-256 hash against the one on GitHub — they did not match. + +### Hardening checklist + +**1. Pin all GitHub Actions to full commit SHAs**, not version tags. Mutable tags are the primary attack vector in Actions supply-chain compromises: the tj-actions/changed-files attacker retroactively updated multiple version tags to reference malicious commits. Use `uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11` instead of `uses: actions/checkout@v4`. + +**2. Set `GITHUB_TOKEN` permissions to read-only by default** at the organization level (Settings → Actions → General → Workflow permissions → Read repository contents and packages permissions). Grant write permissions explicitly per-workflow using the `permissions:` block. + +**3. Never use `pull_request_target` with checkout of the PR head.** The `pull_request_target` trigger runs with full repository secrets and write tokens but can be tricked into executing attacker-controlled code from a fork PR. If `pull_request_target` is required, gate execution behind a maintainer-applied label (e.g., `safe-to-test`) and never check out `github.event.pull_request.head.sha`. The February 2026 "hackerbot-claw" campaign exploited exactly this misconfiguration to steal org-scoped PATs from the Trivy project. + +**4. Restrict fork creation for sensitive repositories** and audit the fork network. GitHub forks share the underlying object store — data from deleted forks or private forks can remain accessible via commit SHA in the parent repository. This is documented behavior, not a bug. Run `trufflehog github-experimental --repo --object-discovery` to scan for cross-fork object reference (CFOR) exposures. + +**5. Implement SLSA Level 2+ build provenance.** Use the `slsa-framework/slsa-github-generator` reusable workflow to generate signed provenance attestations for release artifacts. Verify with `slsa-verifier` or `gh attestation verify` in deployment pipelines. SLSA L3 (hardened, isolated builds) would have made the SolarWinds attack — where malware was injected at compile time by the SUNSPOT implant, not in the source repository — significantly harder. + +**6. Run OpenSSF Scorecard** weekly on all repositories. The automated tool evaluates branch protection, dangerous workflow patterns (including `pull_request_target` misconfigurations), pinned dependencies, token permissions, and signed releases. Integrate via the `ossf/scorecard-action` GitHub Action. + +**7. Review `.gitmodules` changes as high-risk in code review.** Submodule URL manipulation (including CVE-2023-29007, which allowed arbitrary git config injection via overlong submodule URLs) is an active attack vector. Prefer proper package managers over submodules where possible. + +### Residual risk + +The xz-utils pattern — years of social engineering culminating in build-system-only backdoors invisible in git source — is extremely difficult to defend against programmatically. Reproducible builds, multi-maintainer release signing, and comparing source against release tarballs are the best mitigations, but they require significant process maturity. The SLSA framework provides a graduated path toward build integrity. + +--- + +## 6. Git hosting platform hardening + +### Threat model + +**Who:** External attackers targeting organization-wide misconfigurations; insiders exploiting overly permissive defaults. **What:** Mass data exfiltration, privilege escalation, or persistence via platform settings. **How:** Account takeover of admins (Gentoo pattern), exploitation of default-permissive organization settings, or abuse of integration permissions. + +### Hardening checklist + +**1. Enforce 2FA for all organization members.** Available on all GitHub tiers (Free, Team, Enterprise). The Gentoo GitHub compromise (June 2018) was enabled by a single admin account without 2FA — the attacker guessed the password using a predictable password scheme. Non-compliant members are removed (GitHub) or blocked (GitLab). Budget a 2-week compliance window. + +**2. Set base member permissions to "No permission" or "Read"** rather than the default "Write." Grant additional permissions via teams. + +**3. Restrict repository creation to organization owners** or specific teams. Prevent uncontrolled proliferation of repositories with inconsistent security settings. + +**4. Disable forking of private repositories** unless specifically required. All forks of public repos are always public — this cannot be overridden. + +**5. Set default repository visibility to "Private"** (or "Internal" on Enterprise) to prevent accidental public exposure. + +**6. Enable audit log streaming to a SIEM** (see Section 8). GitHub Enterprise streams to Splunk, Datadog, Azure Sentinel, S3, and GCS. GitLab Ultimate streams to HTTP endpoints, S3, GCS, and Azure Event Hubs. + +**7. Apply CIS Benchmarks** where available. The CIS GitHub Benchmark (v1.2.0) and CIS GitLab Benchmark (v1.0.1, with 125+ recommendations) provide audit-ready configuration checklists. GitLab provides an open-source CIS benchmark scanner CLI tool. No standalone CIS benchmark exists for Azure DevOps. + +### Self-hosted versus cloud + +Self-hosting (GitHub Enterprise Server, GitLab self-managed) provides network isolation and data sovereignty but **shifts the patching burden** to the organization. Recent GitLab self-managed CVEs underscore this risk: CVE-2025-25291/25292 (CVSS 8.8, SAML authentication bypass), CVE-2025-6948 (CVSS 8.7, CI/CD pipeline authorization bypass), and CVE-2025-0605 (2FA bypass via Git CLI). For organizations without dedicated security infrastructure teams — the typical Three Backticks Security client — **cloud-hosted platforms are usually more secure** because the vendor handles patching, and the latest security features ship immediately. + +--- + +## 7. Developer workstation git security + +### Threat model + +**Who:** Attackers who can influence repository content (malicious hooks, crafted objects) or access developer machines. **What:** Code execution on developer machines via cloned repositories, credential theft from insecure storage. **How:** Malicious git hooks, crafted git objects exploiting parsing vulnerabilities, symlink attacks, or plaintext credential files. + +**CVE-2024-32002** (Critical) demonstrated this risk: repositories with submodules could trick Git into executing a hook during clone, achieving remote code execution on Windows and macOS. The `git clone` command — seemingly safe — became an attack vector. + +### Hardening checklist + +**1. Use a secure credential helper.** Never use `git-credential-store` (stores credentials as plaintext in `~/.git-credentials`). Per-platform recommendations: **Windows:** Git Credential Manager (bundled with Git for Windows). **macOS:** `osxkeychain` or GCM. **Linux:** `libsecret` or GCM. For CI/CD, use environment variables or short-lived tokens. + +**2. Deploy the hardened `.gitconfig` template** from Appendix A org-wide. Critical directives: `transfer.fsckObjects = true` (verifies integrity of all transferred objects, catching malformed or crafted objects), `safe.bareRepository = explicit` (prevents embedded bare repository attacks), `protocol.git.allow = never` (disables the unencrypted, unauthenticated `git://` protocol), and `core.hooksPath` pointing to a centralized, organization-managed hooks directory. + +**3. Never set `safe.directory = *`.** This wildcard **completely disables** the ownership safety check introduced in Git 2.35.2 (CVE-2022-24765), allowing any user on a shared system to exploit a planted `.git` directory. Add specific trusted directories individually. + +**4. Set `core.hooksPath` to a centralized, version-controlled hooks directory.** This overrides per-repository `.git/hooks/` directories, preventing execution of untrusted hooks from cloned repos. Distribute standardized hooks (pre-commit secret scanning, commit message validation) via this path. + +**5. Enable path traversal protections**: `core.protectNTFS = true` and `core.protectHFS = true` even on Linux servers, to protect developers on mixed-OS teams from NTFS 8.3 short-name attacks (CVE-2019-1352) and HFS+ Unicode normalization attacks. + +### AI coding agents require specific controls + +AI coding agents are now a material attack surface. Claude Code alone accounts for **over 4% of all public GitHub commits** as of March 2026. GitGuardian reports AI-assisted commits leak secrets at **double the baseline rate** (~3.2% vs. ~1.5%). Key risks and mitigations: + +- **Agents bypass pre-commit hooks.** Cursor and Cline are documented to append `--no-verify` to git commits. Mitigation: enforce secret scanning server-side (push protection), not just client-side hooks. Consider the `block-no-verify` package or Claude Code's `PreToolUse` hooks. +- **Agents with overprivileged tokens.** AI agents given broad PATs can be exploited via prompt injection in repository content (issues, README files, code comments, `.cursorrules` files). The IDEsaster research (December 2025) found **100% of tested AI coding tools** vulnerable to prompt injection, with CVE-2025-53773 (CVSS 9.6) affecting GitHub Copilot. Mitigation: scope agent tokens to read-only on specific repositories; use ephemeral, OAuth-scoped credentials. +- **Agents performing destructive operations.** Reports document Cursor running `git push --force-with-lease --no-verify` without permission. Mitigation: use Claude Code's auto mode (which blocks force-pushes via a classifier) or restrict agent git permissions to exclude force-push. +- **62% of AI-generated C programs contain vulnerabilities** (FormAI-v2 dataset, 2024). Treat all AI-generated code as untrusted — enforce the same review requirements as human-authored code. + +--- + +## 8. Audit, monitoring, and incident response + +### Threat model + +**Who:** Any attacker who has gained access — detection depends on monitoring coverage. **What:** Detecting and responding to compromised accounts, unauthorized changes, and data exfiltration. **How:** Audit log analysis, anomaly detection, and git forensics. + +### Hardening checklist + +**1. Enable and stream audit logs.** GitHub Enterprise streams to Splunk, Datadog, Azure Sentinel, S3, and GCS. GitLab Ultimate streams structured JSON to HTTP endpoints, S3, GCS, and Azure Event Hubs. Azure DevOps streams to Log Analytics, Splunk, and Event Grid (90-day default retention; audit streaming must be explicitly enabled — it is off by default). + +**2. Monitor these critical events**: branch protection rule changes (`protected_branch.destroy`), force-pushes to default branches, repository visibility changes to public, new deploy key creation, PAT creation followed by mass cloning (correlated detection), admin role self-assignment, webhook creation/modification, and SSO/2FA policy changes. + +**3. Extend reflog retention for forensic readiness.** Default retention is 90 days (reachable commits) and 30 days (unreachable). Extend with: +``` +git config --global gc.reflogExpire 180.days +git config --global gc.reflogExpireUnreachable 90.days +``` + +**4. Know the forensic toolkit for post-compromise investigation.** `git reflog --date=iso` shows every change to HEAD with timestamps. Force-push events appear as "forced-update" in `git reflog show origin/main`. `git fsck --unreachable --no-reflogs` finds truly orphaned commits that may contain evidence. **Disable garbage collection immediately** during an investigation: `git config gc.auto 0`. + +**5. Integrate git events into existing SIEM detection rules.** Pre-built integrations exist for Microsoft Sentinel (sentinel4github), Datadog Cloud SIEM (native GitHub rules), Panther (Python detection-as-code), Elastic Security (git hook execution detection on Linux endpoints), and Google SecOps (YARA-L rules for GitHub audit logs). + +### Residual risk + +Audit logs capture platform events, not all git operations. A sophisticated attacker who clones a repository via an authorized token and exfiltrates code will appear as normal `git.clone` activity. Anomaly detection (e.g., mass cloning of multiple private repositories in a short window) is the primary detection mechanism for data exfiltration, but tuning thresholds to avoid alert fatigue requires operational investment. + +--- + +## 9. GitHub vs GitLab vs Azure DevOps security feature comparison + +| Security capability | GitHub | GitLab | Azure DevOps | +|---|---|---|---| +| **MFA enforcement** | Org/Enterprise (all tiers); blocks non-compliant members | Instance/Group (all tiers); grace period configurable | Via Entra Conditional Access (not native) | +| **SSO/SAML** | Enterprise Cloud only | All tiers (self-managed); group-level (SaaS) | Native via Entra ID | +| **Fine-grained tokens** | Fine-grained PATs + GitHub Apps | Project/Group tokens | Scoped PATs (no repo-level scoping) | +| **Branch protection** | Rulesets (org-wide) + classic rules | Protected branches + push rules (Premium+) | Branch policies with build validation | +| **Commit signing verification** | ✅ GPG + SSH + S/MIME badges | ✅ GPG + SSH badges | ❌ No native support | +| **Secret scanning** | 200+ types; push protection ($19/committer/mo) | Secret detection; push protection (Ultimate) | Via GHAzDO ($19/committer/mo) | +| **SAST (code scanning)** | CodeQL ($30/committer/mo) | Multiple engines (Ultimate, $99/user/mo) | CodeQL via GHAzDO ($30/committer/mo) | +| **DAST** | Third-party only | ✅ Native (Ultimate) | Third-party only | +| **Container scanning** | Third-party only | ✅ Native (Ultimate) | Third-party only | +| **Fuzz testing** | Third-party only | ✅ Web API fuzzing (Ultimate) | Third-party only | +| **Dependency scanning** | Dependabot (free for alerts) | Native (Ultimate) | Via GHAzDO | +| **Compliance frameworks** | Rulesets | ✅ Centralized frameworks (Ultimate) | Branch policies only | +| **Audit log streaming** | Enterprise only | Ultimate only | Log Analytics, Splunk, Event Grid | +| **IP allowlisting** | Enterprise Cloud | Self-managed (network level) | Entra Conditional Access | +| **CIS Benchmark** | ✅ v1.2.0 | ✅ v1.0.1 (with scanner tool) | ❌ None | +| **Self-hosted option** | Enterprise Server | Self-managed CE/EE | Azure DevOps Server | +| **Security pricing** | Base $21/user + GHAS $49/committer/mo | Ultimate $99/user/mo (all inclusive) | Free base + GHAzDO $49/committer/mo | + +**Key takeaway for Three Backticks Security clients:** GitHub offers the most granular token management (fine-grained PATs, GitHub Apps) and the strongest commit signing ecosystem. GitLab Ultimate provides the broadest native scanner coverage (DAST, container scanning, fuzzing) at a simpler all-inclusive price point. Azure DevOps lags significantly in git-specific security — no commit signing verification, no native MFA enforcement, and limited audit capabilities compared to the other two platforms. + +--- + +## Appendix A: Minimal `.gitconfig` hardening template + +```ini +# ============================================================ +# SECURITY-HARDENED ~/.gitconfig — Three Backticks Security +# Deploy org-wide via configuration management +# ============================================================ + +[user] + name = Your Name + email = your.email@company.com + # Prevent commits without explicit identity + useConfigOnly = true + +[credential] + # OS-native secure storage (never use 'store') + # Windows: manager | macOS: osxkeychain | Linux: libsecret + helper = osxkeychain + +[core] + # Centralized hooks — overrides per-repo .git/hooks/ + hooksPath = ~/.config/git/hooks + # Path traversal protections (enable on ALL platforms) + protectNTFS = true + protectHFS = true + # Disable symlinks to prevent symlink-based attacks + symlinks = false + +[transfer] + # Verify integrity of ALL transferred objects + fsckObjects = true + # Disable bundle URI fetching + bundleURI = false + +[fetch] + fsckObjects = true + prune = true + +[receive] + fsckObjects = true + +[protocol] + version = 2 + +[protocol "file"] + # Restrict local file protocol (CVE-2022-39253) + allow = user + +[protocol "git"] + # DISABLE unencrypted, unauthenticated git:// protocol + allow = never + +[protocol "ext"] + # Disable external transport helpers + allow = never + +[safe] + # Require explicit --git-dir for bare repos + bareRepository = explicit + # NEVER add: directory = * + +[commit] + gpgsign = true + +[tag] + gpgsign = true + +[gpg] + # SSH signing (simpler than GPG) + format = ssh + +[gpg "ssh"] + allowedSignersFile = ~/.config/git/allowed_signers + +[push] + default = current + autoSetupRemote = true + +[init] + templateDir = ~/.config/git/template + defaultBranch = main + +[url "https://"] + # Force HTTPS for any git:// URLs + insteadOf = git:// +``` + +--- + +## Appendix B: Pre-commit hook stack recommendation + +### Recommended stack: gitleaks via the pre-commit framework + +**Why gitleaks over alternatives:** Gitleaks (Go, MIT license, 160+ built-in detectors) is the best general-purpose pre-commit secret scanner for most organizations. It strikes the optimal balance: fast execution (Go binary, no runtime dependencies), low false-positive rate, extensible TOML configuration, and active maintenance. TruffleHog v3 is more comprehensive (800+ detectors, active API verification) but its AGPL license and heavier runtime make it better suited for CI pipeline scanning than developer-workstation pre-commit hooks. detect-secrets (Yelp) excels in legacy codebases with its baseline approach but requires Python. git-secrets (AWS Labs) is AWS-focused and showing reduced maintenance (272+ unresolved issues). + +### Installation via pre-commit framework + +```yaml +# .pre-commit-config.yaml +repos: + - repo: https://github.com/gitleaks/gitleaks + rev: v8.21.2 + hooks: + - id: gitleaks +``` + +```bash +# Install pre-commit framework +pip install pre-commit +# Install hooks in the repository +pre-commit install +# Run against all files (first-time scan) +pre-commit run --all-files +``` + +### Organization-wide deployment via `core.hooksPath` + +For enforcement beyond individual repositories: + +```bash +# Create centralized hooks directory +mkdir -p ~/.config/git/hooks + +# Create pre-commit hook that runs gitleaks +cat > ~/.config/git/hooks/pre-commit << 'EOF' +#!/bin/bash +# Org-wide pre-commit secret scanning +if command -v gitleaks &> /dev/null; then + gitleaks protect --staged --redact --verbose + if [ $? -ne 0 ]; then + echo "❌ Secret detected. Commit blocked." + echo "If false positive, use: SKIP=gitleaks git commit" + exit 1 + fi +fi +EOF +chmod +x ~/.config/git/hooks/pre-commit + +# Set globally +git config --global core.hooksPath ~/.config/git/hooks +``` + +### Complementary CI pipeline scanning + +Add TruffleHog in CI for deeper scanning with active verification: + +```yaml +# GitHub Actions +- name: TruffleHog scan + uses: trufflesecurity/trufflehog@main + with: + extra_args: --only-verified --results=verified,unknown +``` + +### Tool selection matrix + +| Criterion | gitleaks | TruffleHog v3 | detect-secrets | git-secrets | +|---|---|---|---|---| +| **Best role** | Pre-commit hook | CI pipeline scan | Legacy codebase onboarding | AWS-only environments | +| **Speed** | Fast (Go binary) | Moderate (verification adds latency) | Fast (diff-only) | Fast (bash/grep) | +| **False positive rate** | Low | Very low (verification) | Low (baseline filtering) | Moderate | +| **Maintenance status** | Active | Active | Active | Low maintenance | +| **License** | MIT | AGPL 3.0 | Apache 2.0 | Apache 2.0 | +| **Operational cost** | ~1–2s per commit | ~10–30s per scan | ~1s per commit | ~1s per commit | + +The recommended stack for a Three Backticks Security client is: **gitleaks as pre-commit hook** (developer workstation) + **TruffleHog v3 in CI** (verified scanning) + **platform push protection** (server-side enforcement). This three-layer approach provides defense-in-depth against the most common and damaging class of git security vulnerabilities. + +--- + +*Prepared by Three Backticks Security. This document reflects the state of git security tooling and platform features as of March 2026. Platform features, pricing, and tool versions should be verified against current vendor documentation before implementation.* \ No newline at end of file diff --git a/docs/research/Gemini 3.1 Pro report.md b/docs/research/Gemini 3.1 Pro report.md new file mode 100644 index 0000000..cb89025 --- /dev/null +++ b/docs/research/Gemini 3.1 Pro report.md @@ -0,0 +1,368 @@ +# **Git Security Hardening Best Practices: A Practitioner's Reference** + +## **Executive Summary** + +The modern software supply chain is inextricably linked to the integrity, availability, and confidentiality of the version control system. Git, and the centralized hosting platforms architected around it, represent a high-value, highly concentrated attack surface. This ecosystem bridges the critical gap between individual developer workstations and automated production deployments. Compromising this pipeline grants adversaries unprecedented leverage, allowing them to poison release artifacts, exfiltrate proprietary intellectual property, and pivot laterally into cloud infrastructure using harvested CI/CD credentials. +This research report provides an exhaustive, practitioner-oriented analysis of Git security hardening. It spans the entire architectural spectrum: from local workstation .gitconfig configurations and ephemeral AI coding agents, through the access control layers of hosted platforms (GitHub, GitLab, Azure DevOps), and into the cryptographic verification of CI/CD integration points. +Based on an analysis of current threat intelligence, recent high-profile supply chain compromises, and the evolving capabilities of Git platforms, the following represents the ten highest-impact hardening measures. These are ranked by their optimal ratio of systemic risk reduction to organizational implementation effort: + +1. **Implement Pre-Commit Secret Scanning:** Deploy lightweight, regex-based and entropy-based scanners (e.g., Gitleaks) directly on developer workstations. This acts as a primary barrier, blocking the Git commit process in milliseconds before the object is ever created locally, supported by deeper pipeline-based verification (e.g., TruffleHog) to catch bypasses. +2. **Enforce Platform-Level Push Protection:** Activate native secret scanning push protection on Git hosting platforms. This configuration intercepts network payloads and blocks pushes containing identifiable, unencrypted credentials before they are ingested into the remote repository's object database. +3. **Restrict High-Privilege CI/CD Triggers:** Severely limit and audit the use of pull\_request\_target and workflow\_run in GitHub Actions. These triggers execute workflows in the context of the base repository, exposing privileged organization tokens to potentially untrusted, attacker-controlled code submitted via pull requests. +4. **Mandate Branch Protection with Administrator Inclusion:** Enforce required reviews, mandatory status checks, and linear histories on all default branches. Crucially, explicitly configure these rules to apply to repository administrators, closing the prevalent "god-mode" bypass vulnerability. +5. **Transition to Ed25519 SSH Keys:** Deprecate legacy RSA (particularly keys under 2048 bits) and explicitly ban DSA/ECDSA keys. Mandate Ed25519 for all SSH authentication, establish stringent key rotation policies, and strictly avoid plaintext credential storage in local .git-credentials files. +6. **Implement Cryptographic Commit Signing:** Enforce commit integrity using GPG or SSH signing protocols, combined with platform-level "Vigilant Mode" to proactively flag unsigned or cryptographically spoofed commits. +7. **Harden Developer Workstations via GitConfig:** Enable strict object integrity checking (transfer.fsckObjects \= true) to force the local Git client to validate objects, preventing the ingestion of malformed, corrupted, or maliciously crafted Git objects during fetch and clone operations. +8. **Adopt Keyless Signing via Sigstore/Gitsign:** Modernize the verified boot chain by leveraging OpenID Connect (OIDC) ephemeral certificates for commit and artifact signing. This satisfies Supply-chain Levels for Software Artifacts (SLSA) Level 3 provenance requirements without the immense overhead of long-lived cryptographic key management. +9. **Stream Audit Logs to a Modern SIEM:** Export platform audit logs (e.g., GitHub Enterprise audit events) to a Security Information and Event Management (SIEM) system. Construct detection engineering rules to monitor for behavioral anomalies, such as unexpected user role changes, mass repository cloning, or the disabling of security configurations. +10. **Sandbox AI Agent Workflows:** Implement strict environmental boundaries around AI coding agents (e.g., Claude Code, Cursor, Copilot). Restrict network egress and file system access to prevent malicious repository content (e.g., altered test files or markdown documents) from executing prompt injections that hijack the Model Context Protocol (MCP) and exfiltrate local secrets. + +### **Pragmatic "Start Here" Prioritization** + +For organizations with limited security maturity, constrained resources, or those currently lacking dedicated security personnel, implementing the entire spectrum of Git hardening simultaneously is unfeasible and highly disruptive. The following five measures represent the pragmatic minimum baseline that should be implemented immediately to mitigate the most statistically likely avenues of compromise: + +1. **Enable Native Push Protection:** Toggle on GitHub Advanced Security or GitLab Ultimate native secret scanning and push protection globally across the organization. +2. **Lock Down the Default Branch:** Turn on branch protection for main/master, requiring at least one approving review from a peer, and definitively check the box to "Include administrators" in these restrictions. +3. **Deploy Git Credential Manager:** Migrate all developers to Git Credential Manager (GCM) to completely eliminate plaintext .git-credentials files, leveraging native OS encrypted keyrings instead. +4. **Scope and Expire Tokens:** Audit all Personal Access Tokens (PATs). Scope them to specific repositories with strict expiration dates, immediately migrating to fine-grained PATs where the hosting platform supports them. +5. **Pin CI/CD Dependencies:** Pin all third-party GitHub Actions and GitLab CI templates to specific, immutable commit SHAs rather than mutable semantic version tags (e.g., @v2), nullifying the threat of upstream maintainer compromise. + +## **Table of Contents** + +1.(\#secret-exposure) 2\. [Authentication & Access Control](#bookmark=id.65gy2uc5u300) 3\. [Commit Integrity](#bookmark=id.vpln9n4dy5rx) 4.(\#branch-protection--code-review-enforcement) 5.(\#supply-chain-attacks-via-git) 6\. [Git Hosting Platform Hardening](#bookmark=id.xumhkthfbc71) 7.(\#developer-workstation-git-security) 8.(\#audit-monitoring--incident-response) 9.(\#comparative-analysis-hosting-platform-security-parity) 10.(\#appendix-a-minimal-gitconfig-hardening-template) 11.(\#appendix-b-pre-commit-hook-stack-recommendation) + +## **Secret Exposure** + +### **Threat Model** + +The unintentional inclusion of credentials, API keys, database connection strings, and cryptographic tokens in Git repositories remains the most pervasive and easily exploitable vulnerability in the software development lifecycle. The threat landscape is characterized by high-speed automation; threat actors deploy distributed scraping infrastructure that monitors public commit feeds—such as the GitHub Events API—to extract plaintext secrets in near real-time. +A seminal 2019 Network and Distributed System Security Symposium (NDSS) study, "How Bad Can It Git?", empirically demonstrated the severity of this vector. The researchers found that secret leakage affected over 100,000 public repositories, with thousands of unique, valid secrets leaked daily. Crucially, the study established that the median time to discovery for a leaked key on GitHub is precisely 20 seconds, with some scrapers acquiring keys in under half a second. A rigorous manual evaluation confirmed that 89% of the leaked keys were genuinely sensitive and granted unauthorized access to underlying infrastructure. +Current data indicates the problem is accelerating. According to a 2025 GitGuardian state of secrets sprawl report, data analysis of 69.6 million public GitHub repositories revealed a 25% year-over-year increase in new hardcoded secrets, totaling over 23.7 million newly exposed credentials in 2024 alone. Approximately 15% of all commit authors leaked a secret. +A critical component of this threat model is the "false sense of security" surrounding private repositories. Organizations frequently operate under the assumption that repository access controls mitigate the need for strict secret hygiene. However, GitGuardian's telemetry indicates that 35% of scanned private repositories contained at least one plaintext secret, making them nine times more likely to harbor credentials than their public counterparts. Furthermore, the proliferation of AI coding assistants introduces a new leakage vector; repositories actively utilizing AI generation tools (like GitHub Copilot) exhibited a 6.4% secret leakage rate, which is a 40% higher exposure rate than the baseline average across all repositories. + +### **Hardening Measures Checklist** + +1. **Deploy Fast Pre-Commit Scanners Locally:** Install lightweight, regex-based scanners directly on developer workstations. Tools like Gitleaks execute in milliseconds, analyzing the diff and blocking the Git commit process before the local Git object is cryptographically created. +2. **Enable Platform Push Protection:** Configure GitHub Advanced Security or GitLab Ultimate to actively inspect incoming network payloads and reject pushes containing identifiable secrets. This acts as a secondary safety net if local hooks are bypassed via the git commit \--no-verify flag. +3. **Implement CI/CD Live Verification:** Integrate deep-scanning tools like TruffleHog into the CI/CD pipeline. TruffleHog's defining feature is its credential verification engine; it actively attempts to authenticate against the target service (e.g., AWS, Slack) to determine if a detected secret is live, thereby drastically reducing false-positive triage fatigue for security teams. +4. **Maintain Strict .gitignore Hygiene:** Enforce global .gitignore configurations that universally exclude environment files (.env), IDE configuration directories (.vscode/, .idea/), and operating system artifacts (.DS\_Store). + +### **Real-World Motivation: The Persistence Problem** + +When a developer realizes they have accidentally committed a secret, the most common—and entirely ineffective—reaction is to execute a git rm or git revert, commit the deletion, and push to the remote. This creates what the NDSS researchers term the "persistence problem". +Git is an append-only, content-addressable file system. Issuing a deletion command merely removes the file from the working directory of the *current* commit tree. The credential remains permanently accessible in the repository's historical object database. An adversary who monitored the original commit via the Events API already possesses the secret. Furthermore, even if the repository is private, any developer with a local clone retains the unredacted history, creating a high probability that the secret will be accidentally reintroduced during a subsequent branch merge or rebase operation. + +### **Implementation Notes: History Rewriting** + +The only mathematically sound remediation for a leaked credential is to revoke it at the issuing provider. However, if immediate revocation is impossible, or if organizational compliance mandates the sanitization of the Git history, the repository must be rewritten. +Historically, the native git filter-branch command was utilized, but its performance on large repositories is notoriously abysmal, as it steps through every commit and examines the complete file hierarchy sequentially. To solve this, Roberto Tyley created the BFG Repo-Cleaner, a Java-based alternative that utilizes multi-core processing in Scala to rewrite history magnitudes faster. +However, as of recent years, the official Git project strongly recommends git-filter-repo. Written in Python, git-filter-repo manipulates the Git fast-export stream directly, offering unparalleled speed and a versatile library for specialized history rewriting that eclipses both git filter-branch and BFG in usability and safety. +To remove a secret using git-filter-repo, the execution requires a fresh, bare mirror clone to ensure working tree artifacts do not interfere: +`git clone --mirror git://example.com/sensitive-repo.git` +`cd sensitive-repo.git` +`git filter-repo --replace-text <(echo "sensitive_string==>REDACTED")` +`git push --all --force` + +### **Residual Risk** + +Even after successfully executing a force-push with a rewritten history, residual risk remains exceptionally high. Platforms like GitHub aggressively cache objects and pull request references for performance and archival purposes. The deleted commit, though detached from any branch reference, can still be accessed directly via the web interface or API if an attacker possesses the specific SHA-1 hash. Administrators must contact platform support to execute a manual garbage collection cycle to permanently purge the orphaned, cached objects from the vendor's backend infrastructure. + +## **Authentication & Access Control** + +### **Threat Model** + +The compromise of the authentication mechanisms used to access and push code to a repository grants an attacker the immediate ability to introduce malicious code, alter release artifacts, and pivot into the CI/CD infrastructure. Threat actors utilize a variety of vectors to achieve this, including widespread credential stuffing attacks against platform web interfaces, deploying infostealer malware to local workstations to harvest plaintext SSH keys, and identifying exposed, over-privileged Personal Access Tokens (PATs) in internal wikis or Slack channels. The core failure mode is the violation of the principle of least privilege, where a single compromised token grants unrestricted read and write access to an entire organization's portfolio of source code. + +### **Hardening Measures Checklist** + +1. **Migrate to Ed25519 SSH Keys:** Mandate the use of Ed25519 elliptic curve cryptographic algorithms for all SSH key generation, ensuring high performance and small key sizes. +2. **Deprecate Legacy Cryptography:** Explicitly ban the use of RSA (specifically key lengths under 2048-bit), DSA, and ECDSA keys across the organization's infrastructure. +3. **Segregate Key Usage:** Generate distinct, purpose-built keys for authentication versus commit signing, aligning with cryptographic best practices. +4. **Enforce Fine-Grained Tokens:** Replace classic, broadly-scoped PATs with Fine-Grained PATs (on GitHub) or heavily scoped Project Access Tokens (on GitLab). +5. **Implement SSO/SAML Restrictions:** Bind repository access to an enterprise Identity Provider (IdP) and enforce strict IP allowlisting to guarantee that code can only be pushed from secured corporate VPN exit nodes. + +### **Real-World Motivation** + +In June 2018, the Gentoo Linux GitHub organization suffered a severe compromise. Threat actors gained administrative control over the organization, modified the contents of several repositories, and successfully locked out legitimate Gentoo developers, rendering the GitHub mirror unusable for five days. +The root cause of the incident was not a sophisticated zero-day exploit against Git, but rather a predictable password used on a highly privileged administrator account that lacked enforced Multi-Factor Authentication (MFA). Once inside, the attackers attempted to execute destructive commands (e.g., injecting rm \-rf into repositories) and pushed malicious payloads into the ebuilds. This incident starkly highlights the catastrophic blast radius of a single compromised administrative credential and the absolute necessity of platform-enforced 2FA. + +### **Implementation Notes** + +When establishing SSH access for Git operations, organizations must move away from the historical default of RSA. Ed25519 utilizes elliptic curve cryptography based on a specific curve formula rather than the prime factorization methodology of RSA. This allows Ed25519 to require significantly shorter keys for an equivalent level of security while performing cryptographic computations substantially faster. Modern OpenSSH releases (beginning strongly with version 8.7) have actively deprecated legacy algorithms like rsa-sha1. +Furthermore, according to the National Institute of Standards and Technology (NIST) Special Publication 800-57 Part 1 Revision 5, Section 5.2, a single cryptographic key should be strictly limited to one purpose. Administrators should mandate key generation syntax that adheres to these segregation practices: +`# For authentication` +`ssh-keygen -t ed25519 -C "auth_id_ed25519_2026"` +`# For commit signing` +`ssh-keygen -t ed25519 -C "sign_id_ed25519_2026"` + +For API and HTTPS access, classic PATs represent a massive organizational liability. They frequently grant sweeping, organization-wide privileges, lack rigorous expiration controls, and are rarely rotated. Organizations must systematically migrate to Fine-Grained PATs (available in GitHub) which allow administrators to strictly limit token access to specific repositories and enforce absolute, non-extendable expiration dates. For automated CI/CD pipelines, Machine Users or dedicated Deploy Keys (configured as read-only wherever technically possible) must be utilized instead of binding critical automation to an individual human user's token. + +### **Residual Risk** + +Even with the implementation of Ed25519 keys and tightly scoped Fine-Grained PATs, credentials that reside in plaintext on a developer's workstation can be swiftly stolen by modern infostealer malware. Implementing hardware-backed keys (e.g., YubiKey or Titan keys) for SSH authentication, or requiring SSO-backed short-lived certificates via OIDC, fundamentally mitigates this risk. However, transitioning an engineering team to hardware-backed SSH keys introduces high operational friction, increased support desk overhead, and complex recovery scenarios if physical tokens are lost. + +## **Commit Integrity** + +### **Threat Model** + +The underlying Git object model utilizes cryptographic hashing to identify, link, and verify the integrity of commits and file blobs. Historically, Git relied exclusively on the SHA-1 algorithm. In February 2017, the SHAttered attack demonstrated the first practical collision against SHA-1, proving definitively that two entirely different files could be engineered to yield the exact same cryptographic hash. If a sophisticated attacker can generate a collision, they possess the capability to spoof commit history or subtly alter repository contents without breaking the cryptographic chain or triggering integrity alerts. +Compounding this mathematical vulnerability is a logical one: Git relies solely on the user-configured user.name and user.email values in the local .gitconfig to attribute authorship to commits. These values are trivial to spoof, allowing attackers or malicious insiders to seamlessly masquerade as legitimate maintainers, thereby attributing malicious code to trusted developers. + +### **Hardening Measures Checklist** + +1. **Enforce Cryptographic Commit Signing:** Require developers to cryptographically sign all commits locally using GPG, SSH, or S/MIME prior to pushing to the remote. +2. **Enable Platform Vigilant Mode:** Activate "Vigilant Mode" on GitHub (and equivalent settings on GitLab) to proactively flag unsigned commits, or commits signed with unverified keys, with a highly visible "Unverified" badge. +3. **Mandate Signed Commits via Branch Protection:** Configure repository branch protection rules to violently reject any pull request or direct push to protected branches that lacks a mathematically verified signature. +4. **Prepare for SHA-256 Migration:** Begin testing Git 3.0 interoperability features by configuring experimental SHA-256 repositories for non-critical projects to ensure internal tooling is compatible. + +### **Real-World Motivation** + +The ability to masquerade as a legitimate, trusted developer is a core component of social engineering and stealth within open-source and internal enterprise projects. During the 2021 PHP Git server compromise, unknown attackers pushed malicious commits containing a remote code execution backdoor into the php-src repository. To evade immediate detection, the attackers spoofed the Git identities of PHP creator Rasmus Lerdorf and prominent core maintainer Nikita Popov. While this specific incident involved a direct server compromise, the ease with which the authors' identities were forged highlights the absolute necessity of enforcing cryptographic proof of authorship at the individual commit level. + +### **Implementation Notes** + +The Git project is actively navigating a complex migration away from SHA-1. Git releases 2.48 through 2.51 have established the foundational architecture for the eventual Git 3.0 release, introducing SHA-256 as the default hash algorithm for newly initialized repositories. A critical component of this transition is the "interop" mode, designed to allow legacy SHA-1 and modern SHA-256 repositories to communicate and coexist during the multi-year migration period. Organizations should begin auditing their internal Git tooling for SHA-256 compatibility. +Regarding commit signing, organizations must evaluate the trade-offs between GPG and SSH. + +* **SSH Signing:** Introduced in recent Git versions, SSH signing allows developers to leverage their existing Ed25519 authentication keys (or preferably, a segregated SSH signing key) to sign commits (git config \--global gpg.format ssh). This dramatically simplifies deployment and reduces developer friction compared to GPG. However, GitHub's implementation of SSH signing lacks native mechanisms for key expiration and revocation. Once an SSH key is verified by the platform, commits signed with that key remain verified indefinitely, even if the key is later deemed compromised and deleted from the user's account. +* **GPG Signing:** GPG provides a highly robust, decentralized architecture with native support for key expiration, subkeys, and cryptographic revocation certificates. When a GPG key expires or is actively marked as compromised, GitHub automatically updates the historical status of all associated commits to "Unverified". Despite the notoriously steep learning curve and high developer friction, GPG remains the superior choice for high-security and heavily regulated environments. + +### **Residual Risk** + +A critical misconception among developers is that a signed commit guarantees the safety of the code. Commit signing is strictly an identity and integrity control; it proves *who* pushed the code and that the payload was not altered in transit. It provides zero guarantees regarding the *quality* or *safety* of the code itself. A compromised developer machine with an unlocked GPG or SSH key agent active allows an attacker to silently push perfectly signed, cryptographically verified malware into the repository. + +## **Branch Protection & Code Review Enforcement** + +### **Threat Model** + +The default branch of a repository (typically main or master) represents the canonical, authoritative state of the software. In modern DevSecOps environments, this branch is often tied directly to automated CI/CD deployment pipelines. The primary threat involves malicious actors—or negligent developers attempting to bypass friction—pushing code directly to this branch. This action bypasses critical automated testing, static analysis (SAST), dynamic analysis (DAST), and fundamental human oversight, allowing vulnerabilities or backdoors an unobstructed path to the production environment. + +### **Hardening Measures Checklist** + +1. **Require Approving Reviews:** Mandate at least one (and preferably two for critical infrastructure) approving reviews from authorized personnel before a pull request can be merged. +2. **Enforce CODEOWNERS:** Utilize a CODEOWNERS file to automatically request reviews from specific, qualified teams when sensitive files (e.g., CI/CD workflow YAMLs, Terraform configurations, Kubernetes manifests) are modified. +3. **Require Status Checks to Pass:** Configure branch protection to block merges until all designated CI/CD pipelines (linting, SAST, unit tests) execute successfully and return a passing status. +4. **Include Administrators in Rules:** Explicitly check the platform configuration option to enforce all branch protection rules on repository administrators and organization owners. +5. **Dismiss Stale Reviews:** Configure the platform to automatically dismiss all previous approvals when new commits are pushed to an active pull request branch. + +### **Real-World Motivation** + +Branch protection is frequently undermined by subtle misconfigurations. A critical vulnerability disclosed by Legit Security highlighted a severe flaw in how GitHub handled required reviewers. An attacker who had already received legitimate approval on a pull request could push additional, malicious commits to the branch just moments before clicking the "Merge" button. Because the platform did not automatically invalidate the prior approval, the attacker successfully introduced unreviewed, malicious code into the main branch. This emphasizes the absolute necessity of enabling the "Dismiss stale pull request approvals when new commits are pushed" setting. +Furthermore, the Mercari Platform Security Team noted a systemic risk related to collaborative development. In many engineering cultures, developers require cross-repository write access to collaborate effectively. Without strict branch protections, an engineer from a front-end team could arbitrarily overwrite Kubernetes deployment manifests owned by the infrastructure team without any review. + +### **Implementation Notes** + +The single most common failure mode in branch protection implementation is the "admin override." By default, both GitHub and GitLab branch protection rules *do not* apply to users with administrative privileges. In many small-to-medium organizations, senior developers and engineering managers are granted admin rights by default. If the "Include administrators" checkbox is left unchecked, the entire branch protection framework functions merely as a suggestion. It is easily bypassed during high-pressure production incidents, or, catastrophically, by an attacker who compromises a single senior engineer's token. +Organizations must also counter the psychological "rubber stamp" problem, where developers blindly approve pull requests to expedite delivery without reviewing the code. To mitigate this, combine CODEOWNERS with strict team segregation, ensuring that developers cannot approve changes to architecture outside their domain expertise. +The integration of AI agents introduces a novel bypass vector. Organizations must ensure that bot tokens, CI/CD runners, or AI GitHub Apps (e.g., automated PR triage agents) are explicitly restricted from approving their own pull requests or possessing the permissions to bypass required status checks. + +### **Residual Risk** + +Branch protection relies heavily on the assumption that reviewers act with integrity and diligence. A highly sophisticated attacker who compromises two distinct developer accounts can author malicious code with the first account and approve the pull request with the second, successfully bypassing all logical branch protection constraints through synthetic collusion. + +## **Supply Chain Attacks via Git** + +### **Threat Model** + +Git is increasingly leveraged not just as a target, but as the primary delivery mechanism for complex software supply chain compromises. Attackers embed malicious code in obfuscated artifacts, manipulate complex build scripts, or exploit the CI/CD pipeline's implicit trust in the version control system. +A prominent and highly exploitable attack vector involves abusing GitHub Actions triggers. Specifically, the pull\_request\_target and workflow\_run triggers execute workflows in the context of the *base* repository rather than the forked repository. This grants the executing workflow access to the base repository's sensitive secrets and write tokens, allowing untrusted code submitted via a pull request to exfiltrate credentials or poison the pipeline. + +### **Hardening Measures Checklist** + +1. **Restrict High-Privilege Workflows:** Rigorously audit all CI/CD pipelines for the presence of the pull\_request\_target trigger. Ensure that untrusted user input is never evaluated directly in a script execution context, preventing Poisoned Pipeline Execution (PPE). +2. **Pin Action Dependencies:** Pin all third-party GitHub Actions and GitLab CI includes to specific, immutable cryptographic commit SHAs rather than mutable semantic version tags (e.g., @v1), preventing upstream maintainers from pushing malicious updates to existing tags. +3. **Implement SLSA Provenance:** Adopt the Supply-chain Levels for Software Artifacts (SLSA) framework to generate unforgeable build provenance, documenting exactly how and where an artifact was built. +4. **Deploy Keyless Signing (Sigstore):** Utilize the Sigstore ecosystem and Gitsign to establish a cryptographically verified boot chain from the initial Git commit through to the final artifact deployment. + +### **Real-World Motivation** + +The March 2024 xz-utils backdoor (CVE-2024-3094) represents a masterclass in Git-based supply chain compromise. The attacker, operating under the moniker "Jia Tan," spent years building reputation and trust to gain maintainer status over the critical compression library. The execution of the backdoor relied heavily on Git-specific manipulation techniques: + +* The malicious payload was not committed as raw source code. Instead, it was obfuscated and hidden inside binary test files (bad-3-corrupt\_lzma2.xz and good-large\_compressed.lzma) within the Git repository. +* The attacker subtly manipulated the build-to-host.m4 script to unpack the malicious test data and inject an object file during the build process, specifically targeting Debian and Fedora RPM builds that patch their SSH daemon with liblzma. +* Crucially, the release tarballs published upstream contained differing code from the raw Git repository source, effectively bypassing security analysts who solely reviewed the GitHub commit history. + +In CI/CD environments, incidents like the Shai Hulud v2 worm (2025) and GhostAction attacks demonstrated how abusing the pull\_request\_target trigger allows attackers to steal organization-wide tokens, inject malicious workflows, and exponentially replicate the compromise across thousands of repositories. + +### **Implementation Notes** + +Securing the supply chain requires cryptographic assurance that the deployed artifact strictly corresponds to the reviewed source code, and that the build environment was not compromised. The SLSA framework provides a standardized checklist for this assurance. Achieving SLSA Build Level 3 requires isolation between the build process and the calling workflow, ensuring the build instructions cannot be tampered with dynamically. +To implement this assurance at the source level, organizations should deploy **Gitsign**, a tool within the Sigstore ecosystem. Gitsign implements "keyless" signing utilizing OpenID Connect (OIDC). When a developer commits code, Gitsign authenticates them via their IdP (e.g., Google, Microsoft, or GitHub), requests a short-lived, ephemeral certificate from Sigstore's Fulcio Certificate Authority, signs the commit, and logs the public validation material into the Rekor immutable transparency log. +This paradigm entirely eliminates the need to distribute, manage, and rotate long-lived GPG keys in CI/CD environments or on developer machines, while providing an immutable, publicly verifiable record of code provenance that ties the commit directly to a verified corporate identity. + +### **Residual Risk** + +Keyless signing using Sigstore relies fundamentally on the security of the underlying OIDC identity provider. If a developer's SSO identity is compromised (e.g., via a sophisticated adversary-in-the-middle phishing attack that bypasses MFA), the attacker can seamlessly generate valid ephemeral certificates and sign malicious commits that will pass all cryptographic and SLSA verification checks perfectly. + +## **Git Hosting Platform Hardening** + +### **Threat Model** + +The centralized SaaS platform or on-premise server hosting the Git repositories (GitHub, GitLab, Azure DevOps) is a primary apex target for state-sponsored actors and ransomware syndicates. Compromise at this architectural tier grants attackers global visibility into proprietary source code, widespread access to hardcoded secrets, and the ability to manipulate the parameters of the entire CI/CD infrastructure. Vulnerabilities at this level stem from misconfigured overarching organization policies, the lack of enforced multi-factor authentication, inadequate lifecycle management of external contractors, or unpatched self-hosted enterprise server deployments. + +### **Hardening Measures Checklist** + +1. **Enforce Multi-Factor Authentication (MFA):** Mandate MFA for all organization members at the platform level, preventing access to the organization's resources if the user's account lacks 2FA. +2. **Restrict Outside Collaborators:** Implement strict lifecycle management and auditing for external contractors. Utilize platform features to automatically expire external access after a set duration. +3. **Disable Public Forks:** Configure organization-level policies to globally prevent the creation of public forks from private repositories, eliminating a major data exfiltration pathway. +4. **Set Default Visibility to Private:** Ensure that newly created repositories default to private visibility to prevent the accidental public leakage of proprietary code by developers rushing to spin up new projects. + +### **Real-World Motivation** + +In March 2021, the official PHP Git server (git.php.net) was compromised. Because the PHP core maintainers managed their own self-hosted Git infrastructure, an unknown attacker was able to exploit an undisclosed vulnerability in the server itself. The attacker bypassed Git's logical access controls and pushed malicious commits directly into the source tree, attempting to introduce a remote code execution backdoor disguised as a minor typographical fix. +Following an intense incident response investigation, the PHP team determined that maintaining custom, self-hosted Git infrastructure was an unnecessary and unmanageable security risk, prompting them to migrate the project entirely to GitHub. This incident highlights the immense patching burdens, infrastructure security requirements, and inherent dangers associated with self-hosting Git servers compared to utilizing managed SaaS platforms. + +### **Implementation Notes** + +When selecting and hardening a hosting platform, security leaders must understand the architectural philosophies and security paradigms of the primary vendors. +GitHub operates heavily on an ecosystem and modularity model, relying on GitHub Actions and an extensive third-party marketplace for CI/CD integrations and security logic. GitLab takes an integrated, "all-in-one" DevSecOps approach, bundling SAST, DAST, container registries, and compliance frameworks natively into the core product, thereby reducing the necessity for context switching and external tool sprawl. Azure DevOps caters predominantly to enterprise Microsoft environments, providing deep, native integration with Azure Boards for project management and Windows-based deployment targets, while actively porting GitHub's Advanced Security features (CodeQL, secret scanning) into the Azure Repos ecosystem. + +### **Residual Risk** + +Cloud-hosted platforms operate under a strict shared responsibility model. Even with rigorous internal hardening, RBAC configurations, and perfect secret hygiene, a zero-day vulnerability in the infrastructure of GitHub, GitLab, or Azure DevOps could lead to massive, unavoidable data exposure—a systemic risk inherent to utilizing centralized SaaS version control. + +## **Developer Workstation Git Security** + +### **Threat Model** + +The individual developer's workstation is the genesis of all source code and historically the weakest link in the Git security chain. Attackers target developer workstations using spear-phishing, malicious NPM/PyPI typosquatting packages, or complex prompt injection attacks via AI coding agents. A compromised workstation allows an attacker to manipulate local .git directories, alter commit histories and signatures before they are pushed to the remote, or exfiltrate plaintext credentials and environment variables. +A rapidly emerging and highly critical threat vector involves AI coding agents (e.g., Claude Code, Cursor) that utilize the Model Context Protocol (MCP) to autonomously access local file systems, read repository structures, and execute terminal commands. If an AI agent ingests an attacker-controlled repository containing an indirect prompt injection—such as hidden, malicious instructions embedded in a markdown file, a test artifact, or a GitHub issue—the Large Language Model (LLM) can be manipulated into utilizing its sweeping local access to execute commands, exfiltrate SSH keys, or alter local code. +A comprehensive 2026 security audit by Snyk ("ToxicSkills") analyzed nearly 4,000 publicly available AI Agent Skills and found that 13.4% contained critical security flaws, including intentional prompt injection payloads, backdoor installations, and malware distribution mechanisms. The researchers noted that the agent skills ecosystem mirrors the early, vulnerable days of NPM, but with unprecedented access to local file systems and API credentials. + +### **Hardening Measures Checklist** + +1. **Deploy Git Credential Manager (GCM):** Eliminate plaintext credential caching by installing GCM, which securely interfaces with native operating system encrypted keyrings (macOS Keychain, Windows Credential Manager, Linux libsecret). +2. **Harden Object Verification (fsckObjects):** Configure the local .gitconfig to violently validate the cryptographic integrity and structural validity of all incoming objects during network operations. +3. **Enforce safe.directory:** Ensure Git respects directory ownership limits to prevent the execution of malicious repositories or hooks hosted on shared network drives or external media. +4. **Upgrade to Git Protocol v2:** Force the use of Git wire protocol version 2 to enhance network performance and reduce the attack surface by avoiding unnecessary, massive reference advertisements during initial client-server connections. +5. **Sandbox AI Agent Workflows:** Restrict network egress for AI agents running locally to prevent data exfiltration. Block file write operations outside of the designated repository workspace to mitigate the impact of prompt injection attacks attempting to alter global .gitconfig or .bashrc files. + +### **Real-World Motivation** + +Historically, developers cached their HTTPS Git credentials using git config \--global credential.helper store, a default setting that writes the username and authentication token in cleartext to a file named \~/.git-credentials in the user's home directory. Modern infostealer malware is specifically programmed to target and exfiltrate this file, immediately compromising the developer's platform access without triggering anomalous authentication alerts, as the token is entirely valid. +Furthermore, regarding AI agents, researchers from Invariant Labs demonstrated an exploit where an attacker created a malicious GitHub issue in a public repository. When a developer asked their AI assistant to "check the open issues," the agent ingested the malicious issue, processed the embedded prompt injection, and used the developer's local GitHub token to exfiltrate private repository data disguised as helpful analysis. + +### **Implementation Notes** + +The local .gitconfig file must be hardened against object manipulation. Setting transfer.fsckObjects \= true, fetch.fsckObjects \= true, and receive.fsckObjects \= true forces the local Git binary to abort the fetch or receive process immediately if it encounters a malformed object, a manipulated file mode, or a link to a nonexistent object. While this setting does not prevent logical code flaws or malware, it stops an attacker from executing denial-of-service attacks against the local git binary or exploiting potential integer overflows via corrupted, malicious packfiles. +To mitigate the threat of malicious local Git hooks—which execute automatically during operations like commit or push—organizations should standardize the hook execution path using the core.hooksPath directive. By pointing this configuration to a centrally managed, read-only directory controlled by IT, attackers (or malicious cloned repositories) are prevented from dropping malicious shell scripts into the local .git/hooks/pre-commit directory. + +### **Residual Risk** + +If a developer's workstation suffers a full system compromise via malware or an unpatched OS vulnerability, the attacker gains Ring 3 (user-level) or Ring 0 (kernel-level) execution privileges. At this stage, the attacker can manipulate the Git binary in memory, alter configurations, or keylog passphrases, effectively bypassing all Git-specific workstation controls. + +## **Audit, Monitoring & Incident Response** + +### **Threat Model** + +In the chaotic hours during and immediately following a repository compromise, security teams frequently face a severe visibility deficit. Sophisticated attackers will force-push over existing branches to inject code, subsequently delete remote branches to cover their tracks, or rapidly clone multiple proprietary repositories for data exfiltration. Without robust, centralized logging and deep, localized Git forensic knowledge, reconstructing the attack timeline, identifying the exact files exfiltrated, or recovering overwritten code is nearly impossible. + +### **Hardening Measures Checklist** + +1. **Integrate Platform Logs with a SIEM:** Stream GitHub, GitLab, or Azure DevOps audit logs directly into a modern SIEM (e.g., Datadog, Sumo Logic, Panther) for centralized correlation and long-term retention. +2. **Monitor Critical Security Events:** Build explicit detection engineering rules for high-fidelity indicators of compromise within the SIEM. +3. **Master Git Forensics (Reflog):** Train incident responders in local repository forensics, specifically utilizing the git reflog utility to track local state changes and recover "deleted" or overwritten commits. + +### **Real-World Motivation** + +Threat groups increasingly operate by compromising a developer's account, generating a new, highly privileged personal access token, and utilizing an automated script via an anonymizing VPN to rapidly clone all repositories the compromised user has access to. Because native platform UI logs often only display a subset of recent events, are siloed from other security data, and are easily overlooked by busy administrators, security teams require automated SIEM alerts mapped to these specific behavioral anomalies (e.g., mass cloning from anomalous geolocations) to detect data exfiltration before the attacker completes their operation. + +### **Implementation Notes** + +Effective monitoring requires knowing exactly which events indicate a potential breach or architectural weakening. In GitHub Enterprise, security teams should construct SIEM alerts for the following critical audit events : + +* org.update\_actions\_settings: Indicates an alteration to GitHub Actions policies, potentially an attacker enabling workflows on forked repositories. +* workflows.prepared\_workflow\_job: Highly valuable for tracking exactly which organizational secrets were exposed to a specific CI/CD job execution. +* org.sso\_response: Essential for tracking authentication anomalies and issuer details. +* org.set\_workflow\_permission\_can\_approve\_pr: Alerts if the policy preventing GitHub Actions from approving pull requests is disabled. + +When an attacker successfully force-pushes malicious code and subsequently deletes the branch to cover their tracks on the remote server, the standard git log command is useless to an incident responder. git log only displays the history of the current branch pointers. +To recover the data, responders must rely on the **reference log** (git reflog). The reflog is a purely local diary that meticulously records every movement of the HEAD pointer (commits, checkouts, merges, resets). Because Git's garbage collector only purges orphaned commits periodically (typically after 30 to 90 days), the "deleted" commits still exist entirely intact in the local object database. +To recover an overwritten or deleted branch during a live incident: + +1. **Identify the lost commit:** Execute git reflog and locate the state prior to the destructive action (e.g., HEAD@{3}: commit: secure logic). +2. **Extract the hash:** Identify the associated SHA-1 hash (e.g., def5678). +3. **Inspect the state:** git checkout def5678 to verify the code is intact. +4. **Restore the branch:** git checkout \-b recovery-branch def5678. +5. **Remediate:** Force push the recovered branch back to the remote server to cleanly overwrite the attacker's modifications. + +### **Residual Risk** + +Platform audit logs frequently suffer from API latency delays, and native log retention limits (often restricted to 90 or 180 days) severely hinder long-term forensic investigations if the data is not immediately archived to an immutable external SIEM. Furthermore, git reflog is a strictly local construct; if the attacker's destructive actions occurred entirely on the remote server and no local developer machine had recently synced the targeted branches, the local reflogs will not contain the requisite cryptographic hashes for recovery. + +## **Comparative Analysis: Hosting Platform Security Parity** + +Organizations must align their DevSecOps platform selection with their strategic security requirements, compliance mandates, and existing infrastructure. The table below outlines the security feature parity and architectural differences across the three dominant Git hosting platforms as of early 2026\. + +| Security Capability / Feature | GitHub (Advanced Security) | GitLab (Ultimate) | Azure DevOps (Advanced Security) | +| :---- | :---- | :---- | :---- | +| **Secret Scanning (Push Protection)** | Yes (Native, blocks generic & patterned secrets, AI integration) | Yes (Native CI/CD pipeline integration) | Yes (Via GitHub Advanced Security for Azure DevOps) | +| **Code Scanning (SAST)** | Yes (Utilizes CodeQL engine, multi-language support) | Yes (Tightly integrated native scanners inside pipelines) | Yes (Utilizes CodeQL via GHAzDO integration) | +| **Dynamic Analysis (DAST)** | No (Requires integration of third-party Marketplace Actions) | Yes (Native DAST & Container Scanning built-in) | No (Requires custom configuration of third-party pipeline tasks) | +| **Commit Signature Verification** | Yes (Supports GPG, SSH, S/MIME, features Vigilant Mode) | Yes (Supports GPG, SSH, X.509) | Yes (Supported, but with limited UI visibility compared to peers) | +| **CI/CD Architectural Security** | Decentralized (Relies heavily on Marketplace actions, high flexibility) | Integrated (Built-in runners, Docker/K8s native, reduces context switching) | Integrated (Deep, native integration with Azure Pipelines and Releases) | +| **Audit Log SIEM Streaming** | Yes (Available for Enterprise Cloud/Server tiers only) | Yes (Utilizes the Streaming Audit Events feature) | Yes (Streams natively to Azure Event Hub / Log Analytics) | +| **Philosophical Security Focus** | Ecosystem-first, developer familiarity, open-source community integration | All-in-one governance, strict enterprise compliance, tool consolidation | Enterprise traceability, strict branch policies, deep Microsoft Azure integration | + +## **Appendix A: Minimal.gitconfig Hardening Template** + +The following represents an optimized, baseline \~/.gitconfig tailored for developer workstations. This configuration balances cryptographic security and object integrity with operational performance, establishing a secure-by-default local environment. +`[core]` + `# Restrict execution of rogue local repositories and enforce ownership constraints` + `protectNTFS = true` + + `# Point hooks to a centrally managed, read-only directory controlled by IT.` + `# This prevents local repositories from executing embedded, malicious hooks.` + `hooksPath = /usr/local/etc/git-hooks` + +`[transfer]` + `# Enforce strict object integrity checks during fetch/clone operations.` + `# Prevents the ingestion of malformed objects or attempts to exploit integer overflows.` + `fsckObjects = true` + +`[fetch]` + `# Abort fetch operations immediately if corrupted blobs are detected on the network.` + `fsckObjects = true` + +`[receive]` + `# Ensure integrity of received packfiles during pushes.` + `fsckObjects = true` + +`[protocol]` + `# Upgrade wire protocol to v2.` + `# Enhances performance and reduces the attack surface by avoiding unnecessary, massive reference advertisements during initial client-server connections.` + `version = 2` + +`[credential]` + `# Eliminate plaintext storage in ~/.git-credentials.` + `# Utilize OS-native encrypted keyrings (macOS Keychain, Windows Credential Manager, libsecret).` + `helper = manager-core` + +`[commit]` + `# Enforce cryptographic signing for all local commits automatically.` + `gpgsign = true` + +`[gpg]` + `# Utilize modern, lightweight Ed25519 SSH keys for signing rather than complex GPG setups.` + `format = ssh` + +`[user]` + `# Specify the exact signing key to prevent key confusion and enforce identity.` + `signingkey = ~/.ssh/id_ed25519_sign_2026` + +## **Appendix B: Pre-Commit Hook Stack Recommendation** + +To prevent credential leakage effectively without grinding development velocity to a halt, organizations should employ a layered defense architecture utilizing two primary open-source tools: **Gitleaks** and **TruffleHog**. + +### **The Strategy:** + +1. **Workstation (Pre-Commit Phase): Gitleaks.** Gitleaks is written in Go and designed for extreme speed, validating Git history using regex and entropy patterns. It functions optimally as a local pre-commit hook (often orchestrated via the husky or pre-commit frameworks). It executes in milliseconds to block the developer from creating the local commit object if a pattern match is found. +2. **CI/CD Pipeline (Verification Phase): TruffleHog.** TruffleHog requires more computational overhead but possesses a critical enterprise feature: live credential verification. Deployed as a mandatory, blocking CI/CD pipeline step, TruffleHog scans for over 800 secret types. Crucially, it actively queries the provider endpoints (e.g., AWS, GitHub, Slack) to verify if the discovered credential is legitimately active and valid. + +### **Rationale:** + +This dual-stack architecture leverages the distinct strengths of both tools. Gitleaks minimizes developer friction by failing fast locally without relying on external network calls or slowing down the commit process. If a developer uses git commit \--no-verify to intentionally bypass the local hook, or commits via the web UI, TruffleHog catches the secret in the pipeline. By authenticating the credential against the target API, TruffleHog drastically reduces the false-positive noise (e.g., test tokens or revoked keys) that plagues traditional regex scanners, ensuring that security analysts only expend effort triaging genuinely exploitable, live credentials. + +#### **Works cited** + +1\. How Bad Can It Git? Characterizing Secret Leakage in Public GitHub Repositories, https://www.semanticscholar.org/paper/How-Bad-Can-It-Git-Characterizing-Secret-Leakage-in-Meli-McNiece/e43b9221f62b9075357dc53ec3d1edf4d856a38c 2\. How bad can it git? Characterizing secret leakage in public GitHub repositories, https://blog.acolyer.org/2019/04/08/how-bad-can-it-git-characterizing-secret-leakage-in-public-github-repositories/ 3\. State of Secrets Sprawl Report 2025 \- GitGuardian, https://www.gitguardian.com/state-of-secrets-sprawl-report-2025 4\. Gitleaks vs TruffleHog (2026): Secret Scanner Comparison \- AppSec Santa, https://appsecsanta.com/sast-tools/gitleaks-vs-trufflehog 5\. News | metafunctor, https://metafunctor.com/post/ 6\. Datomic is Free \- Hacker News, https://news.ycombinator.com/item?id=35727967 7\. BFG Repo-Cleaner by rtyley \- GitHub Pages, https://rtyley.github.io/bfg-repo-cleaner/ 8\. Rewriting a Git repo to remove secrets from the history \- Simon Willison: TIL, https://til.simonwillison.net/git/rewrite-repo-remove-secrets 9\. newren/git-filter-repo: Quickly rewrite git repository history (filter-branch replacement) \- GitHub, https://github.com/newren/git-filter-repo 10\. Git repo history cleanup \- tried BFG step by step \- but the PR having lot more diffs \- and how to check if password removed from history \- Stack Overflow, https://stackoverflow.com/questions/58469271/git-repo-history-cleanup-tried-bfg-step-by-step-but-the-pr-having-lot-more-d 11\. SSH Key Best Practices for 2025 \- Using ed25519, key rotation, and other best practices, https://www.brandonchecketts.com/archives/ssh-ed25519-key-best-practices-for-2025 12\. Comparing SSH Keys: RSA, DSA, ECDSA, or EdDSA? \- Teleport, https://goteleport.com/blog/comparing-ssh-keys/ 13\. SSH Keys \- Best Practices \- GitHub Gist, https://gist.github.com/ChristopherA/3d6a2f39c4b623a1a287b3fb7e0aa05b 14\. Post Incident Review of Gentoo Hack June 2018 \- YouTube, https://www.youtube.com/watch?v=NXbj1XDwgs8 15\. Gentoo Publishes Incident Report After GitHub Hack \- SecurityWeek, https://www.securityweek.com/gentoo-publishes-incident-report-after-github-hack/ 16\. Et tu, Gentoo? Horrible gits meddle with Linux distro's GitHub code \- The Register, https://www.theregister.com/2018/06/28/gentoo\_linux\_github\_hacked/ 17\. SSH key: rsa vs ed25519 : r/linuxadmin \- Reddit, https://www.reddit.com/r/linuxadmin/comments/1oj864k/ssh\_key\_rsa\_vs\_ed25519/ 18\. hash-function-transition Documentation \- Git, https://git-scm.com/docs/hash-function-transition 19\. Git 3.0: Release Date, Features, and What Developers Need to Know \- DeployHQ, https://www.deployhq.com/blog/git-3-0-on-the-horizon-what-git-users-need-to-know-about-the-next-major-release 20\. Git 2.52-rc0 Starts Working On SHA1-SHA256 Interop, Hints For New Default Branch Name, https://www.phoronix.com/news/Git-2.52-rc0-Released 21\. \`git.php.net\` server compromised, move to GitHub, and delayed updates, https://php.watch/news/2021/03/git-php-net-hack 22\. Backdoor added to PHP source code in Git server breach \- WeLiveSecurity, https://www.welivesecurity.com/2021/03/30/backdoor-php-source-code-git-server-breach/ 23\. Git 2.51: Preparing for the future with SHA-256 \- Help Net Security, https://www.helpnetsecurity.com/2025/08/19/git-2-51-sha-256/ 24\. Release Notes \- OpenSSH, https://www.openssh.com/releasenotes.html 25\. SHA 256 Interoperability: What's Next \- brian m. carlson \- YouTube, https://www.youtube.com/watch?v=OidS-YJxjeo 26\. Back to signing git commits with GPG \- Gabor Javorszky, https://javorszky.co.uk/2024/05/28/back-to-signing-git-commits-with-gpg/ 27\. What's the difference between signing commits with SSH versus GPG? \- Stack Overflow, https://stackoverflow.com/questions/73489997/whats-the-difference-between-signing-commits-with-ssh-versus-gpg 28\. Understanding GitHub's implementation of GPG/SSH · community · Discussion \#144780, https://github.com/orgs/community/discussions/144780 29\. Attackers Can Bypass GitHub Required Reviewers to Submit Malicious Code, https://www.legitsecurity.com/blog/bypassing-github-required-reviewers-to-submit-malicious-code 30\. How to bypass GitHub's Branch Protection \- Mercari, https://engineering.mercari.com/en/blog/entry/20241217-github-branch-protection/ 31\. Managing a branch protection rule \- GitHub Docs, https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule 32\. About protected branches \- GitHub Docs, https://docs.github.com/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches 33\. Allow code owners to review their own PRs · community · Discussion \#14866 \- GitHub, https://github.com/orgs/community/discussions/14866 34\. PromptPwnd: Prompt Injection Vulnerabilities in GitHub Actions Using AI Agents, https://www.aikido.dev/blog/promptpwnd-github-actions-ai-agents 35\. Unpacking Security Scanners for GitHub Actions Workflows \- arXiv, https://arxiv.org/html/2601.14455v2 36\. Hardening GitHub Actions: Lessons from Recent Attacks | Wiz Blog, https://www.wiz.io/blog/github-actions-security-guide 37\. slsa-framework/slsa-verifier: Verify provenance from SLSA compliant builders \- GitHub, https://github.com/slsa-framework/slsa-verifier 38\. Gitsign \- Sigstore, https://docs.sigstore.dev/cosign/signing/gitsign/ 39\. Overview \- Sigstore, https://docs.sigstore.dev/about/overview/ 40\. XZ Utils backdoor \- Wikipedia, https://en.wikipedia.org/wiki/XZ\_Utils\_backdoor 41\. xz-utils backdoor situation (CVE-2024-3094) \- GitHub Gist, https://gist.github.com/thesamesam/223949d5a074ebc3dce9ee78baad9e27 42\. XZ Utils Backdoor | Threat Actor Planned to Inject Further Vulnerabilities \- SentinelOne, https://www.sentinelone.com/blog/xz-utils-backdoor-threat-actor-planned-to-inject-further-vulnerabilities/ 43\. CVE-2024-3094: A Backdoor in XZ Utils Leads to Remote Code Execution \- Picus Security, https://www.picussecurity.com/resource/blog/cve-2024-3094-a-backdoor-in-xz-utils-leads-to-remote-code-execution 44\. Microsoft FAQ and guidance for XZ Utils backdoor, https://techcommunity.microsoft.com/blog/vulnerability-management/microsoft-faq-and-guidance-for-xz-utils-backdoor/4101961 45\. Top 10 GitHub Actions Security Pitfalls: The Ultimate Guide to Bulletproof Workflows \- Arctiq, https://arctiq.com/blog/top-10-github-actions-security-pitfalls-the-ultimate-guide-to-bulletproof-workflows 46\. Frequently asked questions \- SLSA.dev, https://slsa.dev/spec/v1.1/faq 47\. Artifact attestations \- GitHub Docs, https://docs.github.com/en/actions/concepts/security/artifact-attestations 48\. GitHub \- sigstore/gitsign: Keyless Git signing using Sigstore, https://github.com/sigstore/gitsign 49\. Keyless Git commit signing with Gitsign and GitHub Actions \- Chainguard, https://www.chainguard.dev/unchained/keyless-git-commit-signing-with-gitsign-and-github-actions 50\. Using Gitsign for Keyless Git Commit Signing \- Ken Muse, https://www.kenmuse.com/blog/using-gitsign-for-keyless-git-commit-signing/ 51\. ​​GitLab vs GitHub 2025: Which DevOps Platform Wins? \- Strapi, https://strapi.io/blog/gitlab-vs-github-devops-platform-comparison 52\. How does GitHub compare to other DevOps tools?, https://github.com/resources/articles/devops-tools-comparison 53\. GitHub vs GitLab: Key Differences and Which to Choose \- Getint, https://www.getint.io/blog/github-vs-gitlab-which-is-better 54\. GitLab vs GitHub: Key Differences for DevSecOps Teams | Xygeni, https://xygeni.io/blog/gitlab-vs-github-which-platform-fits-devsecops-better/ 55\. GitHub Actions vs. GitLab CI vs. Azure DevOps: CI/CD Comparison | Talentblocks Blog, https://talentblocks.com/blog/github-actions-vs-gitlab-ci-vs-azure-devops-a-comparison-of-ci-cd-tools-for-your-development 56\. Azure DevOps Roadmap | Microsoft Learn, https://learn.microsoft.com/en-us/azure/devops/release-notes/features-timeline 57\. Getting the most out of Azure DevOps and GitHub \- Microsoft for Developers, https://developer.microsoft.com/blog/getting-the-most-out-of-azure-devops-and-github 58\. Prompt Injection Attacks on Agentic Coding Assistants: A Systematic Analysis of Vulnerabilities in Skills, Tools, and Protocol Ecosystems \- arXiv, https://arxiv.org/html/2601.17548v1 59\. MCP Horror Stories: The GitHub Prompt Injection Data Heist \- Docker, https://www.docker.com/blog/mcp-horror-stories-github-prompt-injection/ 60\. Practical Security Guidance for Sandboxing Agentic Workflows and Managing Execution Risk | NVIDIA Technical Blog, https://developer.nvidia.com/blog/practical-security-guidance-for-sandboxing-agentic-workflows-and-managing-execution-risk/ 61\. Snyk Finds Prompt Injection in 36%, 1467 Malicious Payloads in a ToxicSkills Study of Agent Skills Supply Chain Compromise, https://snyk.io/blog/toxicskills-malicious-ai-agent-skills-clawhub/ 62\. Credential Storage \- Git, https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage 63\. git-ecosystem/git-credential-manager \- GitHub, https://github.com/git-ecosystem/git-credential-manager 64\. Git Credential Storage: Cache, Store, and Manage Passwords Securely \- OneUptime, https://oneuptime.com/blog/post/2026-01-24-git-credential-storage/view 65\. protocol-v2 Documentation \- Git, https://git-scm.com/docs/protocol-v2 66\. Git protocol v2: Definition, Examples, and Applications | Graph AI, https://www.graphapp.ai/engineering-glossary/git/git-protocol-v2 67\. What's the difference between Git protocol v1 and v0,v2? \- Stack Overflow, https://stackoverflow.com/questions/66743099/whats-the-difference-between-git-protocol-v1-and-v0-v2 68\. Under the hood: Security architecture of GitHub Agentic Workflows, https://github.blog/ai-and-ml/generative-ai/under-the-hood-security-architecture-of-github-agentic-workflows/ 69\. git-config Documentation \- Git, https://git-scm.com/docs/git-config 70\. git-fsck Documentation \- Git, https://git-scm.com/docs/git-fsck 71\. Security best practices for git users \- Infosec, https://www.infosecinstitute.com/resources/security-awareness/security-best-practices-for-git-users/ 72\. githooks Documentation \- Git, https://git-scm.com/docs/githooks 73\. Git hooks : applying \`git config core.hooksPath\` \- Stack Overflow, https://stackoverflow.com/questions/39332407/git-hooks-applying-git-config-core-hookspath 74\. Visualizing GitHub Audit Log in Microsoft Defender | All things Azure, https://devblogs.microsoft.com/all-things-azure/visualizing-github-audit-log-in-microsoft-defender/ 75\. Secure your ci/cd pipelines from supply chain attacks with Sumo Logic's cloud siem rules, https://www.sumologic.com/blog/secure-azure-devops-github-supply-chain-attacks 76\. Protect Business Critical Applications with GitHub Audit Logs & Modern SIEM \- Panther | The Security Monitoring Platform for the Cloud, https://panther.com/blog/protect-business-critical-applications-with-github-audit-logs-modern-siem 77\. GitLab Audit Events \- Datadog Docs, https://docs.datadoghq.com/integrations/gitlab-audit-events/ 78\. Monitoring for Suspicious GitHub Activity with Google Security Operations (Part 1), https://security.googlecloudcommunity.com/community-blog-42/monitoring-for-suspicious-github-activity-with-google-security-operations-part-1-3874 79\. Audit log events for your enterprise \- GitHub Enterprise Cloud Docs, https://docs.github.com/en/enterprise-cloud@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/audit-log-events-for-your-enterprise 80\. Audit log events for your enterprise \- GitHub Docs, https://docs.github.com/github-ae@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/audit-log-events-for-your-enterprise 81\. Audit log events for your organization \- GitHub Docs, https://docs.github.com/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/audit-log-events-for-your-organization 82\. Reviewing the audit log for your organization \- GitHub Docs, https://docs.github.com/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/reviewing-the-audit-log-for-your-organization 83\. Git Reflog Explained: Recover Deleted Commits & Lost Work \- DEV Community, https://dev.to/itxshakil/git-reflog-explained-recover-deleted-commits-lost-work-i4n 84\. Restore Commit History with Git Reflog | by Marius Ibsen | Aug, 2022 | Medium | Dfind Consulting, https://medium.com/dfind-consulting/restore-commit-history-with-git-reflog-67f7676e902f 85\. Recover Deleted Git Branches / Commits with Git Reference Logs (reflog) \- Frank's Brain, https://franksbrain.com/2019/09/16/recover-deleted-git-branches-commits-with-git-reference-logs-reflog/ 86\. Best way to stop secrets from sneaking into repos? : r/devsecops \- Reddit, https://www.reddit.com/r/devsecops/comments/1ona5bw/best\_way\_to\_stop\_secrets\_from\_sneaking\_into\_repos/ \ No newline at end of file diff --git a/docs/specs/2026-03-31-v0.2.0-expanded-hardening.md b/docs/specs/2026-03-31-v0.2.0-expanded-hardening.md new file mode 100644 index 0000000..0edaa68 --- /dev/null +++ b/docs/specs/2026-03-31-v0.2.0-expanded-hardening.md @@ -0,0 +1,330 @@ +# git-harden.sh v0.2.0 — Expanded Hardening Features + +## Motivation + +Gap analysis of two independent research reports (Claude Opus 4.6 and Gemini 3.1 Pro, March 2026) against the v0.1.0 script identified six feature areas where the script falls short of current best-practice recommendations. All additions follow the existing audit+apply pattern and require no new CLI flags. + +### Source Reports + +- `docs/research/Claude Opus 4.6 report.md` +- `docs/research/Gemini 3.1 Pro report.md` + +## Scope + +All changes are additive — no existing behavior changes. The v0.2.0 script will: + +1. Install a gitleaks pre-commit hook +2. Create and configure a global gitignore +3. Detect plaintext credential files +4. Audit SSH key hygiene +5. Add 8 new git config settings +6. Detect dangerous `safe.directory = *` wildcard + +Version bump: `0.1.0` → `0.2.0`. + +--- + +## Feature 1: Pre-commit Hook Installation (gitleaks) + +### Background + +Both reports rank pre-commit secret scanning as the single most impactful workstation-level defense. The v0.1.0 script sets `core.hooksPath = ~/.config/git/hooks` but installs no hooks, leaving the directory empty. + +### Audit Behavior + +- Check if `~/.config/git/hooks/pre-commit` exists and is executable. +- If it exists, check whether it contains a `gitleaks` invocation (grep for `gitleaks`). +- `[OK]` if pre-commit hook exists and references gitleaks. +- `[WARN]` if pre-commit hook exists but does NOT reference gitleaks (user-managed hook — don't touch). +- `[MISS]` if no pre-commit hook exists. + +### Apply Behavior + +- Check if `gitleaks` is on `$PATH` via `command -v gitleaks`. +- If gitleaks is found and no pre-commit hook exists: + - Create `~/.config/git/hooks/pre-commit` with the following content: + ```bash + #!/usr/bin/env bash + # Installed by git-harden.sh — global pre-commit secret scanning + # To bypass for a single commit: SKIP_GITLEAKS=1 git commit + set -o errexit + set -o nounset + set -o pipefail + + if [ "${SKIP_GITLEAKS:-0}" = "1" ]; then + exit 0 + fi + + if command -v gitleaks >/dev/null 2>&1; then + gitleaks protect --staged --redact --verbose + fi + ``` + - `chmod +x` the hook. +- If gitleaks is NOT found: + - `[WARN]` with install instructions: + - macOS: `brew install gitleaks` + - Linux: `brew install gitleaks` or download from GitHub releases + - Still create the hook script (it guards with `command -v` so it's safe without gitleaks installed). Prompt the user before creating. +- If a pre-commit hook already exists (any content): warn and skip. Do not overwrite user-managed hooks. + +### Bypass Mechanism + +The `SKIP_GITLEAKS=1` environment variable allows a single commit to bypass the hook without `--no-verify` (which skips ALL hooks). This is documented in the hook script itself. + +### Acceptance Criteria + +- `--audit` reports status of pre-commit hook with gitleaks. +- Apply creates a working hook that blocks commits containing secrets. +- Existing user hooks are never overwritten. +- Hook is safe to install even if gitleaks is not yet installed. + +--- + +## Feature 2: Global Gitignore + +### Background + +Both reports stress maintaining comprehensive `.gitignore` patterns for secrets. No amount of scanning catches what was never tracked in the first place. + +### Audit Behavior + +- Check if `core.excludesFile` is set in global git config. +- If not set: `[MISS]`. +- If set: check whether the referenced file contains key security patterns (`.env`, `*.pem`, `*.key`). + - `[OK]` if file exists and contains at least one security pattern. + - `[WARN]` if file exists but lacks security patterns: "Global gitignore at lacks secret patterns (.env, *.pem, *.key). Consider adding them." + +### Apply Behavior + +- If `core.excludesFile` is not set: + - Create `~/.config/git/ignore` with the following patterns: + ```gitignore + # === Security: secrets & credentials === + .env + .env.* + !.env.example + *.pem + *.key + *.p12 + *.pfx + *.jks + credentials.json + service-account*.json + .git-credentials + .netrc + .npmrc + .pypirc + + # === Security: Terraform state (contains secrets) === + *.tfstate + *.tfstate.backup + + # === OS artifacts === + .DS_Store + Thumbs.db + Desktop.ini + + # === IDE artifacts === + .idea/ + .vscode/ + *.swp + *.swo + *~ + ``` + - Set `core.excludesFile = ~/.config/git/ignore` via `apply_git_setting`. +- If `core.excludesFile` is already set to a different path: + - Print `[INFO]` noting the existing path. Do not modify or overwrite. + - If the file lacks security patterns, print `[WARN]` with the missing patterns (informational only — no auto-append). + +### Acceptance Criteria + +- Audit reports whether `core.excludesFile` is configured. +- Audit checks existing gitignore files for security pattern coverage and warns if missing. +- Apply creates the file and sets the config only when nothing is configured. +- Existing configurations are never modified — warnings are informational. +- The `!.env.example` negation allows committing example env files. + +--- + +## Feature 3: Plaintext Credential File Detection + +### Background + +Both reports warn that `git-credential-store` writes passwords to `~/.git-credentials` in plaintext. The Gemini report additionally calls out infostealer malware targeting this file. Adjacent credential files (.netrc, .npmrc, .pypirc) pose similar risks. + +### Audit Behavior (audit-only, no apply action) + +Check for existence and content of these files: + +| File | Detection | Severity | +|------|-----------|----------| +| `~/.git-credentials` | File exists | `[WARN]` — "Plaintext git credentials. Migrate to credential helper (osxkeychain/libsecret) and delete this file." | +| `~/.netrc` | File exists | `[WARN]` — "Plaintext network credentials found. May contain git hosting tokens." | +| `~/.npmrc` | File contains `_authToken=` followed by a non-empty value (regex: `_authToken=.+`) | `[WARN]` — "npm registry token in plaintext. Use `npm config set` with env vars instead." | +| `~/.pypirc` | File contains `password` | `[WARN]` — "PyPI credentials in plaintext. Use keyring or token-based auth instead." | + +### Apply Behavior + +None. The script does not delete or modify user credential files. Warnings are informational only. + +### Section Placement + +New section: "Credential Hygiene" — placed after the existing "Credential Storage" audit. + +### Acceptance Criteria + +- Each detected file produces a specific, actionable warning. +- No false positives on files that don't contain credentials (e.g., .npmrc with only registry URL, no token). +- No files are modified or deleted. + +--- + +## Feature 4: SSH Key Hygiene Audit + +### Background + +Both reports recommend ed25519 exclusively. The Claude report notes GitHub blocks legacy RSA/SHA-1 signatures. The Gemini report recommends banning DSA and ECDSA. + +### Audit Behavior (audit-only, no apply action) + +- Scan `~/.ssh/*.pub` files. +- Additionally, parse `IdentityFile` directives from `~/.ssh/config` (the v0.1.0 script already has this parsing logic in `detect_existing_keys`) and include any referenced `.pub` files not already covered by the glob. +- For each `.pub` file, read the first field to determine key type. +- Use `ssh-keygen -l -f ` to extract bit length for RSA keys. +- Report: + +| Key Type | Verdict | +|----------|---------| +| `ssh-ed25519` | `[OK]` | +| `sk-ssh-ed25519@openssh.com` | `[OK]` | +| `ssh-rsa` with >= 2048 bits | `[WARN]` — "RSA key (%d bits). Consider migrating to ed25519." | +| `ssh-rsa` with < 2048 bits | `[WARN]` — "Weak RSA key (%d bits). Migrate to ed25519 immediately." | +| `ssh-dss` | `[WARN]` — "DSA key (deprecated). Migrate to ed25519." | +| `ecdsa-sha2-*` | `[WARN]` — "ECDSA key. Consider migrating to ed25519." | +| `sk-ecdsa-sha2-*` | `[OK]` — Hardware-backed ECDSA is acceptable. | + +### Apply Behavior + +None. Key migration is too risky to automate. Warnings are informational. + +### Section Placement + +New section: "SSH Key Hygiene" — placed after the existing "SSH Configuration" audit. + +### Acceptance Criteria + +- All `.pub` files in `~/.ssh/` are scanned and classified. +- RSA bit length is correctly extracted. +- No false warnings on ed25519 or ed25519-sk keys. +- No keys are modified or deleted. + +--- + +## Feature 5: Additional Git Config Settings + +### Background + +Eight new settings recommended by one or both reports that the v0.1.0 script does not audit or apply. + +### Settings + +| Setting | Value | Rationale | Report Source | Section | +|---------|-------|-----------|---------------|---------| +| `user.useConfigOnly` | `true` | Prevent commits without explicit identity — forces user.name/email to be set, blocking accidental commits under system defaults | Claude | New: "Identity" | +| `gc.reflogExpire` | `180.days` | Extend reflog retention for forensic readiness (default 90 days) | Claude | New: "Forensic Readiness" | +| `gc.reflogExpireUnreachable` | `90.days` | Extend unreachable reflog retention (default 30 days) | Claude | New: "Forensic Readiness" | +| `transfer.bundleURI` | `false` | Disable bundle URI fetching — reduces attack surface | Claude | Existing: "Object Integrity" | +| `protocol.version` | `2` | Wire protocol v2 — better performance, reduced attack surface from reference advertisements | Gemini | Existing: "Protocol Restrictions" | +| `init.defaultBranch` | `main` | Modern default branch name | Claude | New: "Defaults" | +| `core.symlinks` | `false` | Prevent symlink-based attacks (relevant to CVE-2024-32002). **Interactive-only**: prompt with default=yes, but skip in `-y` mode (may break symlink-dependent workflows like Node.js monorepos) | Claude | Existing: "Filesystem Protection" | +| `fetch.prune` | `true` | Auto-prune stale remote-tracking refs on fetch | Claude | Existing: "Object Integrity" | + +### Audit & Apply Behavior + +Seven of eight follow the existing `audit_git_setting` / `apply_git_setting` pattern. `core.symlinks` is the exception: + +- **Audit**: reports current value like all other settings. +- **Interactive mode**: prompts with default=yes ("Disable symlinks to prevent symlink-based attacks (CVE-2024-32002)? Note: this may break projects that use symlinks, e.g. Node.js monorepos. [Y/n]"). +- **`-y` mode**: skips `core.symlinks` entirely (does not auto-apply). This is because disabling symlinks can silently break real workflows, and `-y` mode should not cause unexpected breakage. + +### Acceptance Criteria + +- All 8 settings appear in audit output under their respective sections. +- All 8 settings are applied (with prompt or auto) in apply mode. +- Existing tests updated to cover new settings. + +--- + +## Feature 6: `safe.directory` Wildcard Detection + +### Background + +The Claude report explicitly warns: "Never set `safe.directory = *`." This wildcard completely disables the ownership safety check introduced in Git 2.35.2 (CVE-2022-24765), allowing any user on a shared system to exploit a planted `.git` directory. + +### Audit Behavior + +- Run `git config --global --get-all safe.directory` and check if any value is `*`. +- `[WARN]` if `*` is found: "safe.directory = * disables ownership checks (CVE-2022-24765). Remove this setting." +- No output if `*` is not found (this is not a setting we apply — absence of `*` is the correct state). + +### Apply Behavior + +- If `*` is detected, prompt the user: "Remove dangerous safe.directory = * setting?" +- If accepted, run `git config --global --unset safe.directory '*'` (note: must handle the case where multiple values exist — use `--unset-all` if needed, but only for the `*` value). + +### Section Placement + +Added to existing "Repository Safety" section. + +### Acceptance Criteria + +- Wildcard detected and warned about in audit mode. +- Apply mode offers to remove it. +- Non-wildcard `safe.directory` entries are not affected. + +--- + +## Audit Section Order (v0.2.0) + +Updated ordering with new sections integrated: + +1. Identity (`user.useConfigOnly`) +2. Object Integrity (existing + `transfer.bundleURI`, `fetch.prune`) +3. Protocol Restrictions (existing + `protocol.version`) +4. Filesystem Protection (existing + `core.symlinks`) +5. Hook Control (existing) +6. **Pre-commit Hook** (new — gitleaks) +7. Repository Safety (existing + `safe.directory` wildcard detection) +8. Pull/Merge Hardening (existing) +9. Transport Security (existing) +10. Credential Storage (existing) +11. **Credential Hygiene** (new — plaintext file detection) +12. **Global Gitignore** (new) +13. **Defaults** (new — `init.defaultBranch`) +14. **Forensic Readiness** (new — reflog retention) +15. Visibility (existing) +16. Signing Configuration (existing) +17. SSH Configuration (existing) +18. **SSH Key Hygiene** (new) + +--- + +## Non-Goals + +- Package manager integration (no `brew install` or `apt install`). +- Modifying or deleting user files (credential files, SSH keys). +- Repository-level hardening (branch protection, CODEOWNERS — these remain in admin recommendations). +- CI/CD pipeline configuration. +- GPG signing support (the script remains SSH-signing focused). + +## Compatibility + +Same as v0.1.0: Bash 3.2+, macOS and Linux. No new dependencies. Gitleaks is optional — the hook is safe without it. + +## Testing + +- Extend existing BATS test suite to cover all new audit checks and apply actions. +- Add container test cases for gitleaks hook installation (with and without gitleaks present). +- Test `safe.directory = *` detection and removal. +- Test credential file detection with mock files. +- Test SSH key hygiene with various key types. diff --git a/git-harden.sh b/git-harden.sh index 4fe8ccf..983aad7 100755 --- a/git-harden.sh +++ b/git-harden.sh @@ -10,10 +10,11 @@ IFS=$'\n\t' # ------------------------------------------------------------------------------ # Constants # ------------------------------------------------------------------------------ -readonly VERSION="0.1.0" +readonly VERSION="0.2.0" readonly BACKUP_DIR="${HOME}/.config/git" readonly HOOKS_DIR="${HOME}/.config/git/hooks" readonly ALLOWED_SIGNERS_FILE="${HOME}/.config/git/allowed_signers" +readonly GLOBAL_GITIGNORE="${HOME}/.config/git/ignore" readonly SSH_DIR="${HOME}/.ssh" readonly SSH_CONFIG="${SSH_DIR}/config" @@ -307,12 +308,18 @@ audit_git_setting() { } audit_git_config() { + print_header "Identity" + audit_git_setting "user.useConfigOnly" "true" + print_header "Object Integrity" audit_git_setting "transfer.fsckObjects" "true" audit_git_setting "fetch.fsckObjects" "true" audit_git_setting "receive.fsckObjects" "true" + audit_git_setting "transfer.bundleURI" "false" + audit_git_setting "fetch.prune" "true" print_header "Protocol Restrictions" + audit_git_setting "protocol.version" "2" audit_git_setting "protocol.allow" "never" audit_git_setting "protocol.https.allow" "always" audit_git_setting "protocol.ssh.allow" "always" @@ -324,6 +331,7 @@ audit_git_config() { audit_git_setting "core.protectNTFS" "true" audit_git_setting "core.protectHFS" "true" audit_git_setting "core.fsmonitor" "false" + audit_git_setting "core.symlinks" "false" print_header "Hook Control" # shellcheck disable=SC2088 # Intentional: git config stores literal ~ @@ -333,6 +341,13 @@ audit_git_config() { audit_git_setting "safe.bareRepository" "explicit" audit_git_setting "submodule.recurse" "false" + # Detect dangerous safe.directory = * wildcard (CVE-2022-24765) + local safe_dirs + safe_dirs="$(git config --global --get-all safe.directory 2>/dev/null || true)" + if printf '%s\n' "$safe_dirs" | grep -qx '\*'; then + print_warn "safe.directory = * disables ownership checks (CVE-2022-24765). Remove this setting." + fi + print_header "Pull/Merge Hardening" audit_git_setting "pull.ff" "only" audit_git_setting "merge.ff" "only" @@ -372,10 +387,181 @@ audit_git_config() { print_warn "credential.helper = $cred_current (expected: $DETECTED_CRED_HELPER)" fi + print_header "Defaults" + audit_git_setting "init.defaultBranch" "main" + + print_header "Forensic Readiness" + audit_git_setting "gc.reflogExpire" "180.days" + audit_git_setting "gc.reflogExpireUnreachable" "90.days" + print_header "Visibility" audit_git_setting "log.showSignature" "true" } +audit_precommit_hook() { + print_header "Pre-commit Hook" + + local hook_path="${HOOKS_DIR}/pre-commit" + + if [ ! -f "$hook_path" ]; then + print_miss "No pre-commit hook at $hook_path" + return + fi + + if [ ! -x "$hook_path" ]; then + print_warn "Pre-commit hook exists but is not executable: $hook_path" + return + fi + + if grep -q 'gitleaks' "$hook_path" 2>/dev/null; then + print_ok "Pre-commit hook with gitleaks at $hook_path" + else + print_warn "Pre-commit hook exists but does not reference gitleaks (user-managed)" + fi +} + +audit_global_gitignore() { + print_header "Global Gitignore" + + local excludes_file + excludes_file="$(git config --global --get core.excludesFile 2>/dev/null || true)" + + if [ -z "$excludes_file" ]; then + print_miss "core.excludesFile (no global gitignore configured)" + return + fi + + # Expand tilde + local expanded_path + expanded_path="${excludes_file/#\~/$HOME}" + + if [ ! -f "$expanded_path" ]; then + print_warn "core.excludesFile = $excludes_file (file does not exist)" + return + fi + + # Check for key security patterns + local has_security_patterns=false + if grep -q '\.env' "$expanded_path" 2>/dev/null && \ + grep -q '\*\.pem' "$expanded_path" 2>/dev/null; then + has_security_patterns=true + fi + + if [ "$has_security_patterns" = true ]; then + print_ok "core.excludesFile = $excludes_file (contains security patterns)" + else + print_warn "core.excludesFile = $excludes_file (lacks secret patterns: .env, *.pem, *.key — consider adding them)" + fi +} + +audit_credential_hygiene() { + print_header "Credential Hygiene" + + # shellcheck disable=SC2088 # Intentional: ~ used as display text + # ~/.git-credentials — plaintext git passwords + if [ -f "${HOME}/.git-credentials" ]; then + print_warn "~/.git-credentials exists (plaintext git credentials — migrate to credential helper and delete this file)" + fi + + # shellcheck disable=SC2088 # Intentional: ~ used as display text + # ~/.netrc — plaintext network credentials + if [ -f "${HOME}/.netrc" ]; then + print_warn "~/.netrc exists (plaintext network credentials — may contain git hosting tokens)" + fi + + # ~/.npmrc — check for actual auth tokens + if [ -f "${HOME}/.npmrc" ]; then + if grep -qE '_authToken=.+' "${HOME}/.npmrc" 2>/dev/null; then + # shellcheck disable=SC2088 # Intentional: ~ used as display text + print_warn "~/.npmrc contains auth token (plaintext npm registry token — use env vars instead)" + fi + fi + + # ~/.pypirc — check for password field + if [ -f "${HOME}/.pypirc" ]; then + if grep -qE '^[[:space:]]*password' "${HOME}/.pypirc" 2>/dev/null; then + # shellcheck disable=SC2088 # Intentional: ~ used as display text + print_warn "~/.pypirc contains password (plaintext PyPI credentials — use keyring or token-based auth)" + fi + fi +} + +audit_ssh_key_hygiene() { + print_header "SSH Key Hygiene" + + local pub_files=() + local seen_files="" + + # Collect ~/.ssh/*.pub files + local f + for f in "${SSH_DIR}"/*.pub; do + [ -f "$f" ] || continue + pub_files+=("$f") + seen_files="${seen_files}|${f}" + done + + # Also collect keys from IdentityFile directives in ~/.ssh/config + if [ -f "$SSH_CONFIG" ]; then + local identity_path + while IFS= read -r identity_path; do + identity_path="$(strip_ssh_value "$identity_path")" + [ -z "$identity_path" ] && continue + identity_path="${identity_path/#\~/$HOME}" + local pub_path="${identity_path}.pub" + if [ -f "$pub_path" ]; then + # Skip if already seen + case "$seen_files" in + *"|${pub_path}"*) continue ;; + esac + pub_files+=("$pub_path") + seen_files="${seen_files}|${pub_path}" + fi + done </dev/null | sed 's/^[[:space:]]*[Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy][Ff][Ii][Ll][Ee][[:space:]=]*//') +EOF + fi + + if [ ${#pub_files[@]} -eq 0 ]; then + print_info "No SSH public keys found" + return + fi + + local key_type bits label + for f in "${pub_files[@]}"; do + key_type="$(awk '{print $1}' "$f" 2>/dev/null || true)" + label="$(basename "$f")" + + case "$key_type" in + ssh-ed25519) + print_ok "SSH key $label (ed25519)" + ;; + sk-ssh-ed25519@openssh.com|sk-ssh-ed25519*) + print_ok "SSH key $label (ed25519-sk, hardware-backed)" + ;; + sk-ecdsa-sha2-nistp256@openssh.com|sk-ecdsa-sha2*) + print_ok "SSH key $label (ecdsa-sk, hardware-backed)" + ;; + ssh-rsa) + bits="$(ssh-keygen -l -f "$f" 2>/dev/null | awk '{print $1}' || true)" + if [ -n "$bits" ] && [ "$bits" -lt 2048 ] 2>/dev/null; then + print_warn "SSH key $label (RSA ${bits}-bit — weak, migrate to ed25519 immediately)" + else + print_warn "SSH key $label (RSA ${bits:-?}-bit — consider migrating to ed25519)" + fi + ;; + ssh-dss) + print_warn "SSH key $label (DSA — deprecated, migrate to ed25519)" + ;; + ecdsa-sha2-*) + print_warn "SSH key $label (ECDSA — consider migrating to ed25519)" + ;; + *) + print_info "SSH key $label (unknown type: $key_type)" + ;; + esac + done +} + audit_signing() { print_header "Signing Configuration" @@ -417,7 +603,7 @@ audit_ssh_directive() { local expected="$2" local current - current="$(grep -i "^[[:space:]]*${directive}[[:space:]]" "$SSH_CONFIG" 2>/dev/null | head -1 | sed 's/^[[:space:]]*[^ ]*[[:space:]]*//' || true)" + current="$(grep -i "^[[:space:]]*${directive}[[:space:]=]" "$SSH_CONFIG" 2>/dev/null | head -1 | sed 's/^[[:space:]]*[^[:space:]=]*[[:space:]=]*//' || true)" current="$(strip_ssh_value "$current")" if [ -z "$current" ]; then @@ -511,12 +697,18 @@ apply_git_setting() { apply_git_config() { print_header "Applying Git Config Hardening" + # Identity + apply_git_setting "user.useConfigOnly" "true" + # Object integrity apply_git_setting "transfer.fsckObjects" "true" apply_git_setting "fetch.fsckObjects" "true" apply_git_setting "receive.fsckObjects" "true" + apply_git_setting "transfer.bundleURI" "false" + apply_git_setting "fetch.prune" "true" # Protocol restrictions + apply_git_setting "protocol.version" "2" apply_git_setting "protocol.allow" "never" apply_git_setting "protocol.https.allow" "always" apply_git_setting "protocol.ssh.allow" "always" @@ -529,6 +721,18 @@ apply_git_config() { apply_git_setting "core.protectHFS" "true" apply_git_setting "core.fsmonitor" "false" + # core.symlinks: interactive-only (may break symlink-dependent workflows) + if [ "$AUTO_YES" = false ]; then + local current_symlinks + current_symlinks="$(git config --global --get core.symlinks 2>/dev/null || true)" + if [ "$current_symlinks" != "false" ]; then + if prompt_yn "Disable symlinks to prevent symlink-based attacks (CVE-2024-32002)? Note: may break projects that use symlinks (e.g. Node.js monorepos)."; then + git config --global core.symlinks false + print_info "Set core.symlinks = false" + fi + fi + fi + # Hook control mkdir -p "$HOOKS_DIR" # shellcheck disable=SC2088 # Intentional: git config stores literal ~ @@ -538,6 +742,17 @@ apply_git_config() { apply_git_setting "safe.bareRepository" "explicit" apply_git_setting "submodule.recurse" "false" + # Remove dangerous safe.directory = * wildcard if present + local safe_dirs + safe_dirs="$(git config --global --get-all safe.directory 2>/dev/null || true)" + if printf '%s\n' "$safe_dirs" | grep -qx '\*'; then + if prompt_yn "Remove dangerous safe.directory = * (disables ownership checks, CVE-2022-24765)?"; then + git config --global --unset 'safe.directory' '\*' 2>/dev/null || \ + git config --global --unset-all 'safe.directory' '\*' 2>/dev/null || true + print_info "Removed safe.directory = *" + fi + fi + # Pull/merge hardening apply_git_setting "pull.ff" "only" apply_git_setting "merge.ff" "only" @@ -568,10 +783,132 @@ apply_git_config() { fi fi + # Defaults + apply_git_setting "init.defaultBranch" "main" + + # Forensic readiness + apply_git_setting "gc.reflogExpire" "180.days" + apply_git_setting "gc.reflogExpireUnreachable" "90.days" + # Visibility apply_git_setting "log.showSignature" "true" } +apply_precommit_hook() { + print_header "Pre-commit Hook (gitleaks)" + + local hook_path="${HOOKS_DIR}/pre-commit" + + # Never overwrite existing hooks + if [ -f "$hook_path" ]; then + if grep -q 'gitleaks' "$hook_path" 2>/dev/null; then + return + fi + print_info "Existing pre-commit hook found — not overwriting" + return + fi + + # Check for gitleaks + local has_gitleaks=false + if command -v gitleaks >/dev/null 2>&1; then + has_gitleaks=true + fi + + if [ "$has_gitleaks" = false ]; then + print_warn "gitleaks not found — install it for pre-commit secret scanning:" + printf ' macOS: brew install gitleaks\n' >&2 + printf ' Linux: brew install gitleaks (or download from GitHub releases)\n' >&2 + fi + + if prompt_yn "Install gitleaks pre-commit hook at $hook_path?"; then + mkdir -p "$HOOKS_DIR" + cat > "$hook_path" << 'HOOK_EOF' +#!/usr/bin/env bash +# Installed by git-harden.sh — global pre-commit secret scanning +# To bypass for a single commit: SKIP_GITLEAKS=1 git commit +set -o errexit +set -o nounset +set -o pipefail + +if [ "${SKIP_GITLEAKS:-0}" = "1" ]; then + exit 0 +fi + +if command -v gitleaks >/dev/null 2>&1; then + gitleaks protect --staged --redact --verbose +fi +HOOK_EOF + chmod +x "$hook_path" + print_info "Installed gitleaks pre-commit hook at $hook_path" + fi +} + +apply_global_gitignore() { + print_header "Global Gitignore" + + local excludes_file + excludes_file="$(git config --global --get core.excludesFile 2>/dev/null || true)" + + if [ -n "$excludes_file" ]; then + local expanded_path + expanded_path="${excludes_file/#\~/$HOME}" + print_info "core.excludesFile already set to $excludes_file" + if [ -f "$expanded_path" ]; then + local has_security_patterns=false + if grep -q '\.env' "$expanded_path" 2>/dev/null && \ + grep -q '\*\.pem' "$expanded_path" 2>/dev/null; then + has_security_patterns=true + fi + if [ "$has_security_patterns" = false ]; then + print_warn "Your global gitignore lacks secret patterns (.env, *.pem, *.key) — consider adding them" + fi + fi + return + fi + + if prompt_yn "Create global gitignore with security patterns at $GLOBAL_GITIGNORE?"; then + mkdir -p "$(dirname "$GLOBAL_GITIGNORE")" + cat > "$GLOBAL_GITIGNORE" << 'GITIGNORE_EOF' +# === Security: secrets & credentials === +.env +.env.* +!.env.example +*.pem +*.key +*.p12 +*.pfx +*.jks +credentials.json +service-account*.json +.git-credentials +.netrc +.npmrc +.pypirc + +# === Security: Terraform state (contains secrets) === +*.tfstate +*.tfstate.backup + +# === OS artifacts === +.DS_Store +Thumbs.db +Desktop.ini + +# === IDE artifacts === +.idea/ +.vscode/ +*.swp +*.swo +*~ +GITIGNORE_EOF + print_info "Created $GLOBAL_GITIGNORE" + + # shellcheck disable=SC2088 # Intentional: git config stores literal ~ + git config --global core.excludesFile "~/.config/git/ignore" + print_info "Set core.excludesFile = ~/.config/git/ignore" + fi +} + apply_signing_config() { print_header "Signing Configuration" @@ -586,12 +923,7 @@ apply_signing_config() { if [ "$AUTO_YES" = true ]; then # In -y mode: only enable signing if key exists if [ "$SIGNING_KEY_FOUND" = true ] && [ -n "$SIGNING_PUB_PATH" ] && [ -f "$SIGNING_PUB_PATH" ]; then - git config --global user.signingkey "$SIGNING_PUB_PATH" - print_info "Set user.signingkey = $SIGNING_PUB_PATH" - apply_git_setting "commit.gpgsign" "true" - apply_git_setting "tag.gpgsign" "true" - apply_git_setting "tag.forceSignAnnotated" "true" - setup_allowed_signers + enable_signing "$SIGNING_PUB_PATH" else print_info "No SSH signing key found. Skipping commit.gpgsign and tag.gpgsign." print_info "Run git-harden.sh interactively (without -y) to set up signing." @@ -660,22 +992,41 @@ detect_existing_keys() { esac fi done </dev/null | sed 's/^[[:space:]]*[Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy][Ff][Ii][Ll][Ee][[:space:]]*//') +$(grep -i '^[[:space:]]*IdentityFile[[:space:]=]' "$SSH_CONFIG" 2>/dev/null | sed 's/^[[:space:]]*[Ii][Dd][Ee][Nn][Tt][Ii][Tt][Yy][Ff][Ii][Ll][Ee][[:space:]=]*//') EOF fi } detect_fido2_hardware() { + # Check via ykman (cross-platform) if [ "$HAS_YKMAN" = true ]; then if ykman info >/dev/null 2>&1; then return 0 fi fi + # Check via fido2-token (Linux) if [ "$HAS_FIDO2_TOKEN" = true ]; then if fido2-token -L 2>/dev/null | grep -q .; then return 0 fi fi + # macOS: check IOKit USB registry for FIDO devices (works without ykman) + if [ "$PLATFORM" = "macos" ]; then + if ioreg -p IOUSB -l 2>/dev/null | grep -qi "fido\|yubikey\|security key\|titan"; then + return 0 + fi + fi + # Linux: check /sys for FIDO HID devices (Yubico vendor 1050) + if [ "$PLATFORM" = "linux" ]; then + if [ -d /sys/bus/hid/devices ]; then + for dev_dir in /sys/bus/hid/devices/*; do + [ -d "$dev_dir" ] || continue + case "$(basename "$dev_dir")" in + *1050:*) return 0 ;; + esac + done + fi + fi return 1 } @@ -690,32 +1041,20 @@ signing_wizard() { if [ "$SIGNING_KEY_FOUND" = true ]; then printf '\n Found existing key: %s\n' "$SIGNING_PUB_PATH" >&2 - if prompt_yn "Use this key for git signing?"; then - git config --global user.signingkey "$SIGNING_PUB_PATH" - print_info "Set user.signingkey = $SIGNING_PUB_PATH" - apply_git_setting "commit.gpgsign" "true" - apply_git_setting "tag.gpgsign" "true" - apply_git_setting "tag.forceSignAnnotated" "true" - setup_allowed_signers + if prompt_yn "Use this key for git signing? (enables commit + tag signing)"; then + enable_signing "$SIGNING_PUB_PATH" return fi fi # Offer key generation options - local has_fido2=false - if detect_fido2_hardware; then - has_fido2=true - fi - printf '\n Signing key options:\n' >&2 printf ' 1) Generate a new ed25519 SSH key (software)\n' >&2 - if [ "$has_fido2" = true ]; then - printf ' 2) Generate a new ed25519-sk SSH key (FIDO2 hardware key)\n' >&2 - fi + printf ' 2) Generate a new ed25519-sk SSH key (FIDO2 hardware key)\n' >&2 printf ' s) Skip signing setup\n' >&2 local choice - printf '\n Choose [1%s/s]: ' "$(if [ "$has_fido2" = true ]; then printf '/2'; fi)" >&2 + printf '\n Choose [1/2/s]: ' >&2 read -r choice &2 + printf ' Please insert your security key and press Enter to continue (or q to go back): ' >&2 + local reply + read -r reply &2 + printf ' Then re-run this script.\n' >&2 + return + fi + fi + printf ' Generating ed25519-sk SSH key (touch your security key when prompted)...\n' >&2 local email @@ -804,8 +1181,14 @@ generate_fido2_key() { mkdir -p "$SSH_DIR" chmod 700 "$SSH_DIR" + # Pass -w on macOS; on Linux the built-in support usually works + local keygen_args=(-t ed25519-sk -C "$email" -f "$key_path") + if [ -n "$sk_provider" ]; then + keygen_args+=(-w "$sk_provider") + fi + # Do NOT suppress stderr — per AC-7 - ssh-keygen -t ed25519-sk -C "$email" -f -- "$key_path" /dev/null | head -1 | sed 's/^[[:space:]]*[^ ]*[[:space:]]*//' || true)" + current="$(grep -i "^[[:space:]]*${directive}[[:space:]=]" "$SSH_CONFIG" 2>/dev/null | head -1 | sed 's/^[[:space:]]*[^[:space:]=]*[[:space:]=]*//' || true)" current="$(strip_ssh_value "$current")" if [ "$current" = "$value" ]; then @@ -873,7 +1256,7 @@ apply_ssh_directive() { # Replace first occurrence of the directive (case-insensitive) local replaced=false while IFS= read -r line || [ -n "$line" ]; do - if [ "$replaced" = false ] && printf '%s' "$line" | grep -qi "^[[:space:]]*${directive}[[:space:]]"; then + if [ "$replaced" = false ] && printf '%s' "$line" | grep -qi "^[[:space:]]*${directive}[[:space:]=]"; then printf '%s %s\n' "$directive" "$value" replaced=true else @@ -924,7 +1307,7 @@ apply_ssh_config() { print_admin_recommendations() { print_header "Admin / Org-Level Recommendations" printf ' These are informational and cannot be applied by this script:\n\n' >&2 - printf ' • Enable branch protection rules on main/master branches\n' >&2 + printf ' • Enable branch protection rules on main branches\n' >&2 printf ' • Enable GitHub vigilant mode (Settings → SSH and GPG keys → Flag unsigned commits)\n' >&2 printf ' • Restrict force-pushes (disable or limit to admins)\n' >&2 printf ' • Rotate personal access tokens regularly; prefer fine-grained tokens\n' >&2 @@ -983,8 +1366,12 @@ main() { AUDIT_MISS=0 audit_git_config + audit_precommit_hook + audit_global_gitignore + audit_credential_hygiene audit_signing audit_ssh_config + audit_ssh_key_hygiene local audit_exit=0 print_audit_report || audit_exit=$? @@ -1011,6 +1398,8 @@ main() { backup_git_config apply_git_config + apply_precommit_hook + apply_global_gitignore apply_signing_config apply_ssh_config print_admin_recommendations diff --git a/test/git-harden.bats b/test/git-harden.bats index 46797ef..27057fa 100755 --- a/test/git-harden.bats +++ b/test/git-harden.bats @@ -515,6 +515,37 @@ SSHEOF grep -q "HashKnownHosts no" "${TEST_HOME}/.ssh/config" } +@test "audit recognises SSH directives using = separator" { + source_functions + + cat > "${TEST_HOME}/.ssh/config" <<'SSHEOF' +StrictHostKeyChecking=accept-new +HashKnownHosts = yes +SSHEOF + + run audit_ssh_directive "StrictHostKeyChecking" "accept-new" + assert_output --partial "[OK]" + + run audit_ssh_directive "HashKnownHosts" "yes" + assert_output --partial "[OK]" +} + +@test "apply skips SSH directives using = separator when value matches" { + cat > "${TEST_HOME}/.ssh/config" <<'SSHEOF' +StrictHostKeyChecking=accept-new +SSHEOF + + source_functions + AUTO_YES=true + + apply_ssh_directive "StrictHostKeyChecking" "accept-new" + + # Should still have exactly one occurrence + local count + count="$(grep -c "StrictHostKeyChecking" "${TEST_HOME}/.ssh/config")" + [ "$count" -eq 1 ] +} + # =========================================================================== # Signing: key detection # =========================================================================== @@ -820,3 +851,302 @@ SSHEOF assert_output --partial "branch protection" assert_output --partial "vigilant mode" } + +# =========================================================================== +# v0.2.0: New git config settings +# =========================================================================== + +@test "audit reports new v0.2.0 settings as MISS on fresh config" { + source_functions + detect_platform + detect_credential_helper + + run audit_git_config + assert_output --partial "user.useConfigOnly" + assert_output --partial "transfer.bundleURI" + assert_output --partial "fetch.prune" + assert_output --partial "protocol.version" + assert_output --partial "init.defaultBranch" + assert_output --partial "gc.reflogExpire" + assert_output --partial "gc.reflogExpireUnreachable" + assert_output --partial "core.symlinks" +} + +@test "-y mode applies new v0.2.0 settings" { + run bash "$SCRIPT" -y + assert_success + + [ "$(git config --global user.useConfigOnly)" = "true" ] + [ "$(git config --global transfer.bundleURI)" = "false" ] + [ "$(git config --global fetch.prune)" = "true" ] + [ "$(git config --global protocol.version)" = "2" ] + [ "$(git config --global init.defaultBranch)" = "main" ] + [ "$(git config --global gc.reflogExpire)" = "180.days" ] + [ "$(git config --global gc.reflogExpireUnreachable)" = "90.days" ] +} + +@test "-y mode does NOT apply core.symlinks" { + run bash "$SCRIPT" -y + assert_success + + local symlinks + symlinks="$(git config --global --get core.symlinks 2>/dev/null || echo "unset")" + [ "$symlinks" = "unset" ] +} + +# =========================================================================== +# v0.2.0: safe.directory wildcard detection +# =========================================================================== + +@test "audit detects safe.directory = * wildcard" { + source_functions + detect_platform + detect_credential_helper + + git config --global safe.directory '*' + + run audit_git_config + assert_output --partial "safe.directory = * disables ownership checks" +} + +@test "audit does not warn without safe.directory wildcard" { + source_functions + detect_platform + detect_credential_helper + + git config --global safe.directory "/some/path" + + run audit_git_config + refute_output --partial "safe.directory = * disables" +} + +@test "-y mode removes safe.directory = * wildcard" { + git config --global safe.directory '*' + + run bash "$SCRIPT" -y + assert_success + + local safe_dirs + safe_dirs="$(git config --global --get-all safe.directory 2>/dev/null || echo "none")" + refute [ "$safe_dirs" = "*" ] +} + +# =========================================================================== +# v0.2.0: Pre-commit hook +# =========================================================================== + +@test "audit reports MISS when no pre-commit hook exists" { + source_functions + + run audit_precommit_hook + assert_output --partial "No pre-commit hook" +} + +@test "audit reports OK when gitleaks hook exists" { + source_functions + + mkdir -p "${HOME}/.config/git/hooks" + cat > "${HOME}/.config/git/hooks/pre-commit" << 'EOF' +#!/usr/bin/env bash +gitleaks protect --staged +EOF + chmod +x "${HOME}/.config/git/hooks/pre-commit" + + run audit_precommit_hook + assert_output --partial "[OK]" + assert_output --partial "gitleaks" +} + +@test "audit reports WARN for non-gitleaks hook" { + source_functions + + mkdir -p "${HOME}/.config/git/hooks" + printf '#!/usr/bin/env bash\necho custom hook\n' > "${HOME}/.config/git/hooks/pre-commit" + chmod +x "${HOME}/.config/git/hooks/pre-commit" + + run audit_precommit_hook + assert_output --partial "does not reference gitleaks" +} + +@test "apply does not overwrite existing pre-commit hook" { + source_functions + AUTO_YES=true + + mkdir -p "${HOME}/.config/git/hooks" + printf '#!/usr/bin/env bash\necho my hook\n' > "${HOME}/.config/git/hooks/pre-commit" + + run apply_precommit_hook + assert_output --partial "not overwriting" + + # Verify original content preserved + run cat "${HOME}/.config/git/hooks/pre-commit" + assert_output --partial "my hook" +} + +# =========================================================================== +# v0.2.0: Global gitignore +# =========================================================================== + +@test "audit reports MISS when no excludesFile configured" { + source_functions + + run audit_global_gitignore + assert_output --partial "no global gitignore configured" +} + +@test "audit reports OK when excludesFile has security patterns" { + source_functions + + mkdir -p "${HOME}/.config/git" + printf '.env\n*.pem\n*.key\n' > "${HOME}/.config/git/ignore" + git config --global core.excludesFile "~/.config/git/ignore" + + run audit_global_gitignore + assert_output --partial "[OK]" + assert_output --partial "contains security patterns" +} + +@test "audit warns when excludesFile lacks security patterns" { + source_functions + + mkdir -p "${HOME}/.config/git" + printf '*.log\n*.tmp\n' > "${HOME}/.config/git/ignore" + git config --global core.excludesFile "~/.config/git/ignore" + + run audit_global_gitignore + assert_output --partial "lacks secret patterns" +} + +@test "-y mode creates global gitignore" { + run bash "$SCRIPT" -y + assert_success + + [ -f "${HOME}/.config/git/ignore" ] + run cat "${HOME}/.config/git/ignore" + assert_output --partial ".env" + assert_output --partial "*.pem" + assert_output --partial "*.key" + assert_output --partial "!.env.example" + + [ "$(git config --global core.excludesFile)" = "~/.config/git/ignore" ] +} + +@test "-y mode skips gitignore when excludesFile already set" { + git config --global core.excludesFile "/some/other/path" + + run bash "$SCRIPT" -y + assert_success + + [ "$(git config --global core.excludesFile)" = "/some/other/path" ] +} + +# =========================================================================== +# v0.2.0: Credential hygiene +# =========================================================================== + +@test "audit warns about ~/.git-credentials" { + source_functions + + printf 'https://user:token@github.com\n' > "${HOME}/.git-credentials" + + run audit_credential_hygiene + assert_output --partial "git-credentials" + assert_output --partial "plaintext" +} + +@test "audit warns about ~/.netrc" { + source_functions + + printf 'machine github.com\nlogin user\npassword token\n' > "${HOME}/.netrc" + + run audit_credential_hygiene + assert_output --partial ".netrc" +} + +@test "audit warns about ~/.npmrc with auth token" { + source_functions + + printf '//registry.npmjs.org/:_authToken=npm_abcdef123456\n' > "${HOME}/.npmrc" + + run audit_credential_hygiene + assert_output --partial "npm registry token" +} + +@test "audit does not warn about ~/.npmrc without token" { + source_functions + + printf 'registry=https://registry.npmjs.org/\n' > "${HOME}/.npmrc" + + run audit_credential_hygiene + refute_output --partial "npm registry token" +} + +@test "audit warns about ~/.pypirc with password" { + source_functions + + printf '[pypi]\nusername = user\npassword = secret123\n' > "${HOME}/.pypirc" + + run audit_credential_hygiene + assert_output --partial "PyPI credentials" +} + +@test "audit no warnings with clean credential state" { + source_functions + + run audit_credential_hygiene + refute_output --partial "[WARN]" +} + +# =========================================================================== +# v0.2.0: SSH key hygiene +# =========================================================================== + +@test "SSH key hygiene: ed25519 reported as OK" { + source_functions + + ssh-keygen -t ed25519 -f "${HOME}/.ssh/test_ed25519" -N "" -q + run audit_ssh_key_hygiene + assert_output --partial "[OK]" + assert_output --partial "ed25519" +} + +@test "SSH key hygiene: RSA key reported as WARN" { + source_functions + + ssh-keygen -t rsa -b 2048 -f "${HOME}/.ssh/test_rsa" -N "" -q + run audit_ssh_key_hygiene + assert_output --partial "[WARN]" + assert_output --partial "RSA" + assert_output --partial "migrating to ed25519" +} + +@test "SSH key hygiene: no keys produces info message" { + source_functions + + # Remove the default keys created in setup (there are none) + rm -f "${HOME}/.ssh/"*.pub + + run audit_ssh_key_hygiene + assert_output --partial "No SSH public keys found" +} + +@test "SSH key hygiene: picks up keys from IdentityFile in ssh config" { + source_functions + + mkdir -p "${HOME}/.ssh/custom" + ssh-keygen -t ed25519 -f "${HOME}/.ssh/custom/my_key" -N "" -q + printf 'IdentityFile ~/.ssh/custom/my_key\n' > "${HOME}/.ssh/config" + + run audit_ssh_key_hygiene + assert_output --partial "[OK]" + assert_output --partial "my_key" +} + +# =========================================================================== +# v0.2.0: Version bump +# =========================================================================== + +@test "--version reports 0.2.0" { + run bash "$SCRIPT" --version + assert_output --partial "0.2.0" +} diff --git a/test/interactive/helpers.sh b/test/interactive/helpers.sh index 8a4f2c0..e39a408 100755 --- a/test/interactive/helpers.sh +++ b/test/interactive/helpers.sh @@ -6,7 +6,7 @@ set -o nounset set -o pipefail IFS=$'\n\t' -readonly TMUX_SESSION="test" +TMUX_SESSION="test-$$" readonly SCRIPT_PATH="${HOME}/git-harden.sh" # Colors @@ -43,10 +43,24 @@ send() { tmux send-keys -t "$TMUX_SESSION" "$@" } -# Start git-harden.sh in a tmux session +# Start git-harden.sh in a tmux session. +# Explicitly pass HOME and GIT_CONFIG_GLOBAL — tmux spawns a login shell +# which resets HOME from the passwd entry, breaking the isolated test env. start_session() { tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true - tmux new-session -d -s "$TMUX_SESSION" "bash ${SCRIPT_PATH}" + sleep 0.5 + tmux new-session -d -s "$TMUX_SESSION" \ + "export HOME='${HOME}'; export GIT_CONFIG_GLOBAL='${GIT_CONFIG_GLOBAL:-}'; bash '${SCRIPT_PATH}'" + # Keep the pane alive after the script exits so capture_output can read it + tmux set-option -t "$TMUX_SESSION" remain-on-exit on + sleep 0.5 + # Verify session started + if ! tmux has-session -t "$TMUX_SESSION" 2>/dev/null; then + printf 'ERROR: tmux session "%s" failed to start\n' "$TMUX_SESSION" >&2 + printf 'SCRIPT_PATH=%s\n' "$SCRIPT_PATH" >&2 + printf 'HOME=%s\n' "$HOME" >&2 + return 1 + fi } # Wait for the script to exit and capture final output diff --git a/test/interactive/test-full-accept.sh b/test/interactive/test-full-accept.sh index 00cc909..3e3b3eb 100755 --- a/test/interactive/test-full-accept.sh +++ b/test/interactive/test-full-accept.sh @@ -26,9 +26,11 @@ main() { wait_for "Proceed with hardening" send "y" Enter - # Accept each setting prompt by sending "y" + Enter repeatedly + # Accept each setting prompt by sending "y" + Enter repeatedly. + # v0.2.0 adds more prompts (pre-commit hook, gitignore, core.symlinks), + # so we need enough iterations to get through all of them. local pane_content - for _ in $(seq 1 30); do + for _ in $(seq 1 50); do sleep 0.3 pane_content="$(tmux capture-pane -t "$TMUX_SESSION" -p 2>/dev/null || true)" if printf '%s' "$pane_content" | grep -qF "Signing key options"; then @@ -41,7 +43,7 @@ main() { done # Signing wizard — skip - wait_for "Signing key options" 15 + wait_for "Signing key options" 20 send "s" Enter # Wait for completion diff --git a/test/interactive/test-signing-generate.sh b/test/interactive/test-signing-generate.sh index f2a48b0..8ad2185 100755 --- a/test/interactive/test-signing-generate.sh +++ b/test/interactive/test-signing-generate.sh @@ -29,9 +29,9 @@ main() { wait_for "Proceed with hardening" send "y" Enter - # Accept settings until signing wizard + # Accept settings until signing wizard (v0.2.0 adds more prompts) local pane_content - for _ in $(seq 1 30); do + for _ in $(seq 1 50); do sleep 0.3 pane_content="$(tmux capture-pane -t "$TMUX_SESSION" -p 2>/dev/null || true)" if printf '%s' "$pane_content" | grep -qF "Signing key options"; then @@ -44,7 +44,7 @@ main() { done # Signing wizard — option 1: generate ed25519 - wait_for "Signing key options" 15 + wait_for "Signing key options" 20 send "1" Enter # ssh-keygen prompts for passphrase — enter empty twice @@ -53,6 +53,10 @@ main() { wait_for "Enter same passphrase" 10 send "" Enter + # Signing wizard asks "Enable commit and tag signing?" — accept + wait_for "Enable commit and tag signing" 10 + send "y" Enter + # Wait for completion sleep 3 capture_output >/dev/null 2>&1 || true diff --git a/test/interactive/test-signing-skip.sh b/test/interactive/test-signing-skip.sh index 10159a6..99f5173 100755 --- a/test/interactive/test-signing-skip.sh +++ b/test/interactive/test-signing-skip.sh @@ -16,6 +16,12 @@ main() { printf 'Test: Signing wizard - skip\n' >&2 + # Remove any keys from prior tests so wizard shows key generation options + rm -f "${HOME}/.ssh/id_ed25519" "${HOME}/.ssh/id_ed25519.pub" + rm -f "${HOME}/.ssh/id_ed25519_sk" "${HOME}/.ssh/id_ed25519_sk.pub" + git config --global --unset user.signingkey 2>/dev/null || true + git config --global --unset commit.gpgsign 2>/dev/null || true + start_session # Safety review gate @@ -26,9 +32,9 @@ main() { wait_for "Proceed with hardening" send "y" Enter - # Accept settings until signing wizard + # Accept settings until signing wizard (v0.2.0 adds more prompts) local pane_content - for _ in $(seq 1 30); do + for _ in $(seq 1 50); do sleep 0.3 pane_content="$(tmux capture-pane -t "$TMUX_SESSION" -p 2>/dev/null || true)" if printf '%s' "$pane_content" | grep -qF "Signing key options"; then @@ -41,7 +47,7 @@ main() { done # Signing wizard — skip - wait_for "Signing key options" 15 + wait_for "Signing key options" 20 send "s" Enter # Wait for completion