git-security-tool 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. git_security_tool-0.1.0/.github/workflows/ci.yml +50 -0
  2. git_security_tool-0.1.0/.github/workflows/release.yml +41 -0
  3. git_security_tool-0.1.0/.github/workflows/scan.reusable.yml +46 -0
  4. git_security_tool-0.1.0/.gitignore +17 -0
  5. git_security_tool-0.1.0/ARCHITECTURE.md +279 -0
  6. git_security_tool-0.1.0/CLAUDE.md +64 -0
  7. git_security_tool-0.1.0/LICENSE +21 -0
  8. git_security_tool-0.1.0/PKG-INFO +149 -0
  9. git_security_tool-0.1.0/README.md +120 -0
  10. git_security_tool-0.1.0/pyproject.toml +62 -0
  11. git_security_tool-0.1.0/src/git_security/__init__.py +3 -0
  12. git_security_tool-0.1.0/src/git_security/__main__.py +5 -0
  13. git_security_tool-0.1.0/src/git_security/baseline.py +67 -0
  14. git_security_tool-0.1.0/src/git_security/cli.py +87 -0
  15. git_security_tool-0.1.0/src/git_security/config/__init__.py +0 -0
  16. git_security_tool-0.1.0/src/git_security/config/loader.py +127 -0
  17. git_security_tool-0.1.0/src/git_security/git/__init__.py +0 -0
  18. git_security_tool-0.1.0/src/git_security/git/diff.py +50 -0
  19. git_security_tool-0.1.0/src/git_security/git/hooks.py +25 -0
  20. git_security_tool-0.1.0/src/git_security/git/repository.py +38 -0
  21. git_security_tool-0.1.0/src/git_security/ignore.py +27 -0
  22. git_security_tool-0.1.0/src/git_security/installer/__init__.py +0 -0
  23. git_security_tool-0.1.0/src/git_security/installer/dependencies.py +14 -0
  24. git_security_tool-0.1.0/src/git_security/installer/git_hook.py +122 -0
  25. git_security_tool-0.1.0/src/git_security/models/__init__.py +0 -0
  26. git_security_tool-0.1.0/src/git_security/models/finding.py +33 -0
  27. git_security_tool-0.1.0/src/git_security/policy/__init__.py +0 -0
  28. git_security_tool-0.1.0/src/git_security/policy/engine.py +38 -0
  29. git_security_tool-0.1.0/src/git_security/reporter/__init__.py +0 -0
  30. git_security_tool-0.1.0/src/git_security/reporter/sarif.py +72 -0
  31. git_security_tool-0.1.0/src/git_security/reporter/terminal.py +46 -0
  32. git_security_tool-0.1.0/src/git_security/rules/__init__.py +0 -0
  33. git_security_tool-0.1.0/src/git_security/rules/semgrep/crypto_tls.yml +37 -0
  34. git_security_tool-0.1.0/src/git_security/rules/semgrep/deserialization.yml +38 -0
  35. git_security_tool-0.1.0/src/git_security/rules/semgrep/filesystem_net.yml +38 -0
  36. git_security_tool-0.1.0/src/git_security/rules/semgrep/injection.yml +43 -0
  37. git_security_tool-0.1.0/src/git_security/rules/semgrep/web.yml +32 -0
  38. git_security_tool-0.1.0/src/git_security/scan.py +245 -0
  39. git_security_tool-0.1.0/src/git_security/scanners/__init__.py +0 -0
  40. git_security_tool-0.1.0/src/git_security/scanners/base.py +34 -0
  41. git_security_tool-0.1.0/src/git_security/scanners/gitleaks.py +59 -0
  42. git_security_tool-0.1.0/src/git_security/scanners/ruff.py +103 -0
  43. git_security_tool-0.1.0/src/git_security/scanners/semgrep.py +81 -0
  44. git_security_tool-0.1.0/src/git_security/suggestions/__init__.py +0 -0
  45. git_security_tool-0.1.0/src/git_security/suggestions/llm.py +67 -0
  46. git_security_tool-0.1.0/src/git_security/suggestions/providers.py +110 -0
  47. git_security_tool-0.1.0/tests/test_baseline.py +59 -0
  48. git_security_tool-0.1.0/tests/test_cli.py +49 -0
  49. git_security_tool-0.1.0/tests/test_config.py +104 -0
  50. git_security_tool-0.1.0/tests/test_diff.py +59 -0
  51. git_security_tool-0.1.0/tests/test_ignore.py +33 -0
  52. git_security_tool-0.1.0/tests/test_installer.py +85 -0
  53. git_security_tool-0.1.0/tests/test_policy.py +64 -0
  54. git_security_tool-0.1.0/tests/test_providers.py +76 -0
  55. git_security_tool-0.1.0/tests/test_rules.py +45 -0
  56. git_security_tool-0.1.0/tests/test_sarif.py +52 -0
  57. git_security_tool-0.1.0/tests/test_scan.py +184 -0
  58. git_security_tool-0.1.0/tests/test_scanners.py +117 -0
  59. git_security_tool-0.1.0/tests/test_suggestions.py +48 -0
@@ -0,0 +1,50 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+ security-events: write # for the SARIF upload
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Install
22
+ run: pip install -e ".[scanners,dev]"
23
+
24
+ - name: Install gitleaks
25
+ run: |
26
+ url=$(curl -s https://api.github.com/repos/gitleaks/gitleaks/releases/latest \
27
+ | grep -o 'https://[^"]*linux_x64\.tar\.gz' | head -1)
28
+ curl -sL "$url" | tar xz -C /usr/local/bin gitleaks
29
+ gitleaks version
30
+
31
+ - name: Lint
32
+ run: |
33
+ ruff check src tests
34
+ ruff format --check src tests
35
+
36
+ - name: Test
37
+ run: pytest -q
38
+
39
+ - name: Self-scan (full repo)
40
+ run: git-security-tool scan --all
41
+
42
+ - name: SARIF report
43
+ if: always()
44
+ run: git-security-tool scan --all --format sarif > results.sarif || true
45
+
46
+ - name: Upload SARIF to code scanning
47
+ if: always()
48
+ uses: github/codeql-action/upload-sarif@v3
49
+ with:
50
+ sarif_file: results.sarif
@@ -0,0 +1,41 @@
1
+ name: release
2
+
3
+ # Publishes to PyPI on a version tag (v*), using PyPI Trusted Publishing
4
+ # (OIDC) - no API token stored as a secret.
5
+ #
6
+ # One-time setup on pypi.org:
7
+ # Project "git-security-tool" -> Publishing -> add a trusted publisher:
8
+ # owner: MustafaBasit521 · repo: commit-guard
9
+ # workflow: release.yml · environment: pypi
10
+
11
+ on:
12
+ push:
13
+ tags: ["v*"]
14
+
15
+ jobs:
16
+ build:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - uses: actions/checkout@v4
20
+ - uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+ - run: pip install build
24
+ - run: python -m build
25
+ - uses: actions/upload-artifact@v4
26
+ with:
27
+ name: dist
28
+ path: dist/
29
+
30
+ publish:
31
+ needs: build
32
+ runs-on: ubuntu-latest
33
+ environment: pypi
34
+ permissions:
35
+ id-token: write # for trusted publishing
36
+ steps:
37
+ - uses: actions/download-artifact@v4
38
+ with:
39
+ name: dist
40
+ path: dist/
41
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,46 @@
1
+ name: git-security-tool scan
2
+
3
+ # Reusable workflow. Other repos call it with:
4
+ #
5
+ # jobs:
6
+ # security:
7
+ # uses: MustafaBasit521/commit-guard/.github/workflows/scan.reusable.yml@main
8
+ #
9
+ # Pin @v0.1.0 (or a commit SHA) instead of @main once the tool is tagged, so a
10
+ # change to the tool can't unexpectedly break a downstream project's CI.
11
+
12
+ on:
13
+ workflow_call:
14
+ inputs:
15
+ ref:
16
+ description: "git-security-tool version, branch, or SHA to install"
17
+ type: string
18
+ default: main
19
+ python-version:
20
+ type: string
21
+ default: "3.12"
22
+
23
+ jobs:
24
+ scan:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{ inputs.python-version }}
32
+
33
+ - name: Install git-security-tool
34
+ run: >
35
+ pip install "git-security-tool[scanners] @
36
+ git+https://github.com/MustafaBasit521/commit-guard.git@${{ inputs.ref }}"
37
+
38
+ - name: Install gitleaks
39
+ run: |
40
+ url=$(curl -s https://api.github.com/repos/gitleaks/gitleaks/releases/latest \
41
+ | grep -o 'https://[^"]*linux_x64\.tar\.gz' | head -1)
42
+ curl -sL "$url" | tar xz -C /usr/local/bin gitleaks
43
+ gitleaks version
44
+
45
+ - name: Scan
46
+ run: git-security-tool scan --all
@@ -0,0 +1,17 @@
1
+ # Python
2
+ __pycache__/
3
+ *.pyc
4
+
5
+ # Virtual environments
6
+ .venv/
7
+ venv/
8
+
9
+ # Packaging / build
10
+ build/
11
+ dist/
12
+ *.egg-info/
13
+
14
+ # Tool caches
15
+ .pytest_cache/
16
+ .ruff_cache/
17
+ .git-security-tool.toml
@@ -0,0 +1,279 @@
1
+ # git-security-tool — Architecture
2
+
3
+ This is the as-built reference. It also doubles as a design retrospective:
4
+ each section explains **what** a piece does and **why** it ended up that way.
5
+ For usage, see `README.md`.
6
+
7
+ ---
8
+
9
+ ## 1. What it is
10
+
11
+ A local Git **pre-commit gate** for Linux. On `git commit` it scans the
12
+ *staged* changes; if policy says a finding is serious enough, the commit is
13
+ aborted (`exit 1`). The same engine runs in CI over the whole repo.
14
+
15
+ It is an **orchestration layer**, not a scanner. Detection is delegated to
16
+ three mature tools; this project decides what to scan, runs them, normalizes
17
+ their output, applies policy, and reports.
18
+
19
+ ```
20
+ git commit
21
+ └─► .git/hooks/pre-commit (3-line shell shim)
22
+ └─► git-security-tool scan
23
+ ├─ ruff check → lint (LOW → warn)
24
+ ├─ ruff format --check → formatting (LOW → warn)
25
+ ├─ semgrep + 17 rules → insecure code (HIGH/MEDIUM)
26
+ └─ gitleaks → secrets (CRITICAL → block)
27
+ └─► policy.evaluate() → Decision
28
+ └─► exit 0 (allow) | exit 1 (block)
29
+ ```
30
+
31
+ Core design rule, held throughout: **the exit code is the entire control
32
+ mechanism.** Everything else is logic for choosing between `0` and `1`.
33
+
34
+ ---
35
+
36
+ ## 2. Package layout
37
+
38
+ ```
39
+ src/git_security/
40
+ ├── __main__.py python -m git_security → cli.main()
41
+ ├── cli.py argparse; dispatch to an action; return an exit code
42
+ ├── scan.py the pipeline: gather → scan → policy → report → exit
43
+
44
+ ├── git/
45
+ │ ├── repository.py run_git() — the ONLY place that shells to git
46
+ │ ├── diff.py staged files, staged diff, materialize_staged, ls-files
47
+ │ └── hooks.py resolve this repo's hooks dir / pre-commit path
48
+
49
+ ├── scanners/
50
+ │ ├── base.py run_tool() (missing-tool tolerant), to_repo_relative()
51
+ │ ├── ruff.py run_ruff (lint) + run_ruff_format → list[Finding]
52
+ │ ├── gitleaks.py run_gitleaks(staged=…) → list[Finding]
53
+ │ └── semgrep.py run_semgrep(files, root, rules_dir) → list[Finding]
54
+
55
+ ├── models/finding.py Severity (IntEnum) + Finding (frozen dataclass)
56
+ ├── policy/engine.py PolicyConfig, Decision, evaluate()
57
+ ├── config/loader.py .git-security-tool.toml → Config (+ ConfigError)
58
+ ├── ignore.py glob/prefix path-ignore matching
59
+ ├── baseline.py suppress findings already recorded in a baseline file
60
+
61
+ ├── reporter/
62
+ │ ├── terminal.py Decision → human text
63
+ │ └── sarif.py findings → SARIF 2.1.0 JSON (for CI code scanning)
64
+
65
+ ├── installer/
66
+ │ ├── git_hook.py install / uninstall / status; the managed hook text
67
+ │ └── dependencies.py which scanners are on PATH
68
+
69
+ ├── suggestions/
70
+ │ ├── llm.py optional AI explanation of blocking findings
71
+ │ └── providers.py AnthropicProvider, GeminiProvider (same 4 methods)
72
+
73
+ └── rules/semgrep/*.yml 17 bundled rules, shipped as package data
74
+ ```
75
+
76
+ **Why a package and not one file:** every module has one job and one reason
77
+ to change. `cli.py` reads like a table of contents; `scan.py` is pure
78
+ orchestration; each scanner owns exactly one tool's quirks. Adding a
79
+ capability is usually one new module + one line in `scan.py`.
80
+
81
+ ---
82
+
83
+ ## 3. The data flow
84
+
85
+ ### 3.1 Get the files
86
+
87
+ `git/diff.py` provides three sources:
88
+
89
+ | function | used by | returns |
90
+ |---|---|---|
91
+ | `get_staged_files()` | pre-commit (`scan`) | staged paths, `--diff-filter=ACM` (no deletions) |
92
+ | `get_tracked_files()` | CI (`scan --all`) | `git ls-files` |
93
+ | `materialize_staged(files, dest)` | pre-commit only | writes the **index** blob content into a temp dir via `git checkout-index` |
94
+
95
+ **Why `materialize_staged`:** a commit is built from the *index*, not the
96
+ working tree. Stage `x.py`, edit it again, and a working-tree scan would
97
+ flag code that isn't being committed *and* miss code that is — the dangerous
98
+ direction for a security gate. So `scan` (staged mode) copies the exact
99
+ staged bytes to a throwaway dir and scans that. `scan --all` has no "staged"
100
+ concept, so it scans the working tree in place. Gitleaks is Git-aware and
101
+ scans the index itself (`gitleaks git --staged`), so it never needs the
102
+ temp dir.
103
+
104
+ `_copy_project_config()` also drops the repo's `pyproject.toml` / `ruff.toml`
105
+ into the temp dir — scanners resolve config by walking up from each file, and
106
+ a bare temp dir would make Ruff silently use built-in defaults.
107
+
108
+ ### 3.2 Run the scanners
109
+
110
+ `scan._collect_findings(scope)` runs each enabled scanner through
111
+ `_safe_scan()` — a wrapper that turns a scanner crash (`RuntimeError`) into
112
+ a printed message + empty list, so one broken tool doesn't take down the
113
+ commit with a traceback. A scanner that isn't installed is skipped silently
114
+ by `base.run_tool()` returning `None`.
115
+
116
+ ### 3.3 Normalize — the `Finding` model
117
+
118
+ ```python
119
+ @dataclass(frozen=True)
120
+ class Finding:
121
+ tool: str; rule: str; severity: Severity
122
+ file: str; line: int; message: str
123
+ ```
124
+
125
+ Each scanner's `_to_finding()` converts its tool's native JSON into this.
126
+ The three tools emit wildly different shapes:
127
+
128
+ | | Ruff | Gitleaks | Semgrep |
129
+ |---|---|---|---|
130
+ | path key | `filename` (absolute) | `File` (relative) | `path` |
131
+ | line | `location.row` | `StartLine` | `start.line` |
132
+ | rule | `code` (can be null) | `RuleID` | `check_id` (path-namespaced) |
133
+ | severity | *(none — assigned)* | *(none — CRITICAL)* | `extra.severity` (ERROR/WARNING/INFO) |
134
+
135
+ Absorbing that asymmetry in one place per scanner is the whole point — from
136
+ here on, policy and reporting see only `Finding`.
137
+
138
+ `Severity` is an `IntEnum` so policy can write `f.severity >= threshold`
139
+ directly (it's `4 >= 3` underneath) while still being a typo-proof named
140
+ constant.
141
+
142
+ ### 3.4 Baseline + ignores
143
+
144
+ Before policy:
145
+
146
+ - `ignore.filter_ignored()` drops files matching `[ignore] paths` patterns
147
+ (`tests/fixtures/`, `*.generated.py`, …). Gitleaks findings are filtered
148
+ after the fact since it scans everything.
149
+ - `baseline.apply_baseline()` removes findings whose
150
+ `(tool, rule, file, message)` fingerprint is already in
151
+ `.git-security-tool-baseline.json`. This lets an established repo adopt the
152
+ tool without a big-bang cleanup: existing issues are grandfathered, new
153
+ ones still block. The fingerprint deliberately excludes line number so
154
+ unrelated edits don't resurface a baselined finding.
155
+
156
+ ### 3.5 Policy
157
+
158
+ ```python
159
+ def evaluate(findings, config) -> Decision:
160
+ blocking = [f for f in findings if f.severity >= config.block_threshold]
161
+ warnings = [f for f in findings if f.severity < config.block_threshold]
162
+ return Decision(blocked=bool(blocking), blocking=..., warnings=...)
163
+ ```
164
+
165
+ One pure function. No printing, no `sys.exit`, no subprocess — which is why
166
+ it's trivially unit-tested with fake `Finding`s. `block_threshold` defaults
167
+ to `HIGH` (secrets + RCE-class patterns block; style warns) and is
168
+ overridable per repo.
169
+
170
+ **Why a whole module for ten lines:** it is the single place blocking
171
+ behaviour is defined. Per-directory thresholds, per-rule allowlists,
172
+ "never block on Fridays" — all of that would land here, and scanners /
173
+ `scan.py` wouldn't change.
174
+
175
+ ### 3.6 Report + exit
176
+
177
+ - text mode → `reporter/terminal.py` groups warnings then blocking findings,
178
+ sorted by `(file, line, tool)`, long messages collapsed to one line.
179
+ - sarif mode (`--format sarif`) → `reporter/sarif.py` prints SARIF 2.1.0 to
180
+ **stdout** (progress goes to stderr so the JSON is clean); CI uploads it
181
+ to GitHub code scanning.
182
+
183
+ Then `run_scan` returns `0` or `1`. In staged mode, `GIT_SECURITY_NO_BLOCK=1`
184
+ forces `0` while still reporting — the dev escape hatch (distinct from
185
+ `git commit --no-verify`, which skips the hook entirely).
186
+
187
+ ---
188
+
189
+ ## 4. The other entry points
190
+
191
+ - **`git-security-tool install`** — `installer/git_hook.py` writes a 3-line
192
+ shell shim to this repo's hooks dir (resolved via
193
+ `git rev-parse --git-path hooks`, so `core.hooksPath` / worktrees work).
194
+ The shim carries a marker comment; `install` refuses to overwrite a hook
195
+ it didn't write unless `--force`, and `uninstall` only removes its own.
196
+ - **`git-security-tool baseline`** — runs a full-repo scan and writes every
197
+ current finding to the baseline file.
198
+ - **`git-security-tool check`** — is the hook installed, which scanners are
199
+ present.
200
+
201
+ ---
202
+
203
+ ## 5. Optional AI suggestions
204
+
205
+ Off by default. Enabled only when `[ai] enabled = true` **and** the provider's
206
+ API key is set. `providers.py` has `AnthropicProvider` and `GeminiProvider`
207
+ behind one 4-method shape (`name`, `default_model`, `available()`,
208
+ `complete()`); `[ai] provider` picks one. Gemini uses stdlib `urllib` (no
209
+ dependency); Anthropic needs `pip install ".[ai]"`.
210
+
211
+ Guarantees: it never changes the block decision, never writes files,
212
+ announces before sending code to the API, and **never sends a file Gitleaks
213
+ flagged** (a secret could be in the surrounding snippet).
214
+
215
+ ---
216
+
217
+ ## 6. Two defense layers
218
+
219
+ | | local | CI |
220
+ |---|---|---|
221
+ | trigger | `git commit` | `git push` / PR |
222
+ | command | `git-security-tool scan` (staged) | `git-security-tool scan --all` |
223
+ | bypass | `--no-verify`, or not installed | none |
224
+ | purpose | fast feedback, before it's in history | enforcement nobody can skip |
225
+
226
+ `.github/workflows/scan.reusable.yml` is a `workflow_call` reusable workflow
227
+ so downstream repos reference it in three lines instead of copy-pasting.
228
+
229
+ ---
230
+
231
+ ## 7. Build history — milestone by milestone
232
+
233
+ Each milestone was: explain the concept → smallest implementation → explain
234
+ the code → test → next. The lesson each one carried:
235
+
236
+ | # | Milestone | What it established |
237
+ |---|---|---|
238
+ | 1 | Hook runs a Python program | Git finds an executable `pre-commit`; exit 0/non-zero is the whole gate; hooks aren't version-controlled |
239
+ | 2 | `subprocess` captures staged changes | argument lists (never `shell=True`); `git diff --cached` ≠ `git diff`; `CompletedProcess` |
240
+ | 3 | Ruff wrapper | linter exit codes (1 = "found issues" is normal, not failure); JSON output over text scraping |
241
+ | 4 | Gitleaks wrapper | a second tool with a different invocation model and a different JSON shape → motivates normalization |
242
+ | 5 | `Finding` model + `scanners/base.py` | two concrete tool outputs = the right time to design the common one; extract the shared helper, keep the per-tool quirks |
243
+ | 6 | Semgrep | source → AST → pattern match → structured finding; first tool with real severities; offline (`--metrics=off`) |
244
+ | 7 | Policy engine | `Severity` becomes an ordered `IntEnum`; `evaluate()` is pure; **the exit code finally means something** |
245
+ | 8 | Reporter module + pytest | pull presentation out of `main`; the pure policy/mapper code begs for tests |
246
+ | 9 | Packaging | `pyproject.toml` (hatchling), `[project.scripts]` creates the command, rules ship as package data, `-e` editable install |
247
+ | 10 | `install` / `uninstall` / `check` | the hook must be written per-repo; only ever touch a hook we created (marker + `--force`) |
248
+ | 11 | Config file | `tomllib` (stdlib, 3.11+); reuse `PolicyConfig`; malformed config = clean message, not traceback |
249
+ | 12 | Scan the staged blob | the accuracy bug: index vs working tree; `git checkout-index` to a temp dir; copy project config so scanners keep their settings |
250
+ | 13 | Path ignores | `fnmatch` glob + directory prefix; filter Gitleaks findings after the fact |
251
+ | 14 | `ruff format --check` | linting ≠ formatting; the tool must pass its own checks (reformatted the codebase) |
252
+ | 15 | Optional AI | advisory only; opt-in; announce before sending; never send secret-bearing files |
253
+ | — | Gemini provider | provider-pluggable behind one shape; stdlib `urllib` keeps core zero-dependency |
254
+ | — | `scan --all` + CI | CI has no "staged" — scan tracked files in place; the second, unbypassable layer |
255
+ | — | 17 Semgrep rules | injection / deserialization / crypto-TLS / web / filesystem-net; validated in CI |
256
+ | — | Baseline + SARIF | adopt-on-a-dirty-repo; GitHub code-scanning integration |
257
+ | — | Hardening | `ConfigError` handling, per-scanner isolation (`_safe_scan`), AI secret-leak guard, fixed Gitleaks 8.30 (`protect`/`detect` removed → `git --staged` / `dir`) |
258
+
259
+ ### Mistakes worth remembering
260
+
261
+ - **`git reset --hard` on a commit that held real work** wiped milestone 7;
262
+ recovered from reflog. `--hard` discards commits *and* uncommitted changes.
263
+ - **Staging a scratch file alongside real work** made the "undo the probe"
264
+ step also undo the milestone. Test with the probe unstaged, or `git add`
265
+ only the real files.
266
+ - **`--amend` after pushing** diverged local/remote; fixed with
267
+ `push --force-with-lease` (safe here — solo repo).
268
+ - **Ruff's shifting defaults** kept flagging our own code (`PLW1510`,
269
+ `TRY004`); fixed by pinning `[tool.ruff.lint] select`.
270
+
271
+ ---
272
+
273
+ ## 8. Scope boundaries
274
+
275
+ Not in scope: dependency-CVE scanning, license checks, SBOM, IaC/container
276
+ scanning, non-Python static analysis. The bundled Semgrep set is curated and
277
+ small — not a replacement for the Semgrep registry or a full SAST platform.
278
+ Kubernetes / microservices / a database were explicitly ruled out from day
279
+ one and never needed.
@@ -0,0 +1,64 @@
1
+ # git-security-tool — Claude Code instructions
2
+
3
+ A local Git **pre-commit gate** (Linux) that scans staged changes and blocks
4
+ the commit on serious findings. It is an **orchestration layer** over Ruff,
5
+ Semgrep, and Gitleaks — never reimplement what those tools do. Full design:
6
+ `./ARCHITECTURE.md`. Usage: `./README.md`.
7
+
8
+ ## Status
9
+
10
+ Built and working (milestone 15 + extras). Installed and dogfooded on a real
11
+ project. Not yet tagged / published to PyPI.
12
+
13
+ ## Dev commands
14
+
15
+ ```bash
16
+ pip install -e ".[scanners,dev]" # tool + ruff + semgrep + pytest
17
+ pytest # ~50s (semgrep subprocess tests)
18
+ ruff check src tests && ruff format --check src tests
19
+ git-security-tool scan --all # run the tool on this repo
20
+ ```
21
+
22
+ Python 3.11+. Package import name `git_security`; CLI / PyPI name
23
+ `git-security-tool`.
24
+
25
+ ## Hard rules (the tool must obey its own advice)
26
+
27
+ - **Scan staged content, not the working tree** — `scan` materializes the
28
+ index into a temp dir. `git diff --cached`, never `git diff`.
29
+ - **subprocess: argument lists only.** Never `shell=True`, never build a
30
+ command from untrusted input. `git/repository.py::run_git` is the only
31
+ place that shells to git.
32
+ - **The exit code is the gate.** `run_scan` returns 0 (allow) or 1 (block);
33
+ nothing else decides.
34
+ - **Policy lives in `policy/engine.py`.** Scanners assign a severity and stop;
35
+ they never decide blocking.
36
+ - **AI is advisory** — off by default, opt-in, never changes the decision,
37
+ never writes files, never sends a Gitleaks-flagged file to the API.
38
+ - **Never print/log secret values** (Gitleaks runs with `--redact`).
39
+ - Keep `ruff check` and `ruff format --check` green on `src/` and `tests/`.
40
+
41
+ ## Conventions
42
+
43
+ - One module, one job. New capability = new module + one line in `scan.py`.
44
+ - Each scanner owns its tool's CLI flags, exit-code meaning, and JSON→Finding
45
+ mapping. Shared plumbing (`run_tool`, `to_repo_relative`) is in
46
+ `scanners/base.py`.
47
+ - Config errors raise `ConfigError` → caught in `run_scan` → clean message,
48
+ no traceback.
49
+ - A scanner crash is caught per-scanner (`_safe_scan`) — report it, continue.
50
+ - Tests: pure logic (policy, mappers, config, ignore, baseline, sarif) has no
51
+ I/O; end-to-end tests use a real temp git repo; semgrep/gitleaks tests skip
52
+ if the tool isn't installed.
53
+
54
+ ## Out of scope
55
+
56
+ Dependency-CVE scanning, license/SBOM, IaC/container scanning, non-Python
57
+ static analysis, Kubernetes/microservices/databases. The bundled Semgrep set
58
+ is intentionally small.
59
+
60
+ ## Working style (learning project)
61
+
62
+ Incremental. For a new concept: explain what/why/how it fits → implement the
63
+ smallest version → explain the key code → test → next. Surface any decision
64
+ that affects architecture before acting. No large unexplained code dumps.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Muhammad Mustafa Basit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,149 @@
1
+ Metadata-Version: 2.5
2
+ Name: git-security-tool
3
+ Version: 0.1.0
4
+ Summary: Local Git security and code-quality gate that runs on pre-commit.
5
+ Project-URL: Homepage, https://github.com/MustafaBasit521/commit-guard
6
+ Project-URL: Issues, https://github.com/MustafaBasit521/commit-guard/issues
7
+ Author: Muhammad Mustafa Basit
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: git,gitleaks,pre-commit,sast,secrets,security,semgrep
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Topic :: Security
17
+ Classifier: Topic :: Software Development :: Quality Assurance
18
+ Classifier: Topic :: Software Development :: Version Control :: Git
19
+ Requires-Python: >=3.11
20
+ Provides-Extra: ai
21
+ Requires-Dist: anthropic; extra == 'ai'
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest; extra == 'dev'
24
+ Requires-Dist: ruff; extra == 'dev'
25
+ Provides-Extra: scanners
26
+ Requires-Dist: ruff; extra == 'scanners'
27
+ Requires-Dist: semgrep; extra == 'scanners'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # git-security-tool
31
+
32
+ [![CI](https://github.com/MustafaBasit521/commit-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/MustafaBasit521/commit-guard/actions/workflows/ci.yml)
33
+
34
+ A local Git **pre-commit gate** for Linux. It scans your *staged* changes and
35
+ blocks the commit when it finds something serious — secrets, dangerous code
36
+ patterns — while surfacing quality and formatting issues as warnings.
37
+
38
+ It is an **orchestration layer**, not a new scanner: it runs
39
+ [Gitleaks](https://github.com/gitleaks/gitleaks),
40
+ [Semgrep](https://semgrep.dev/), and [Ruff](https://docs.astral.sh/ruff/),
41
+ normalizes their output, applies your policy, and decides pass/block.
42
+
43
+ ```
44
+ git commit
45
+ └─► .git/hooks/pre-commit
46
+ └─► git-security-tool scan
47
+ ├─ gitleaks → secrets (CRITICAL → blocks)
48
+ ├─ semgrep → security patterns (HIGH → blocks)
49
+ ├─ ruff check → lint (LOW → warns)
50
+ └─ ruff format --check → format (LOW → warns)
51
+ └─► PASS (exit 0) / BLOCK (exit 1)
52
+ ```
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ pip install "git-security-tool[scanners]" # tool + ruff + semgrep
58
+ cd your-repo
59
+ git-security-tool install # writes .git/hooks/pre-commit
60
+ ```
61
+
62
+ Latest unreleased version, straight from the repo:
63
+
64
+ ```bash
65
+ pip install "git-security-tool[scanners] @ git+https://github.com/MustafaBasit521/commit-guard.git"
66
+ ```
67
+
68
+ Gitleaks is a Go binary — install it separately if you want secret detection
69
+ (the scan skips any tool that isn't on `PATH`).
70
+
71
+ Commands: `scan [--all] [--format sarif]`, `baseline`, `install [--force]`,
72
+ `uninstall`, `check`, `version`.
73
+
74
+ - `scan` — staged changes (pre-commit)
75
+ - `scan --all` — every tracked file (CI / audit); `--format sarif` emits SARIF
76
+ on stdout for GitHub code scanning
77
+ - `baseline` — records current findings to `.git-security-tool-baseline.json`
78
+ so a repo can adopt the tool without fixing everything first; new issues
79
+ still block
80
+
81
+ ## What it checks
82
+
83
+ | category | tool | severity | blocks by default |
84
+ |---|---|---|---|
85
+ | Secrets / credentials | Gitleaks (staged diff) | CRITICAL | yes |
86
+ | Insecure code patterns (17 rules) | Semgrep + bundled rules | HIGH / MEDIUM | HIGH yes |
87
+ | Lint (unused imports, undefined names, …) | `ruff check` | LOW | no |
88
+ | Formatting | `ruff format --check` | LOW | no |
89
+
90
+ The bundled Semgrep rules (`src/git_security/rules/semgrep/`) cover code/command
91
+ injection (`eval`, `exec`, `os.system`, `shell=True`), unsafe deserialization
92
+ (`pickle`, `yaml.load`, insecure XML), weak crypto & disabled TLS verification,
93
+ web footguns (Flask `debug=True`, Jinja autoescape off, `mark_safe`), and
94
+ filesystem/network hygiene (`extractall`, `mktemp`, `requests` without timeout).
95
+
96
+ Semgrep/Ruff analysis is **Python only**; Gitleaks is language-agnostic.
97
+ Scanners see the exact **staged** content, not your working tree.
98
+
99
+ ## Configuration — `.git-security-tool.toml` (optional, repo root)
100
+
101
+ ```toml
102
+ [policy]
103
+ block_threshold = "HIGH" # INFO | LOW | MEDIUM | HIGH | CRITICAL
104
+
105
+ [scanners]
106
+ gitleaks = false # disable a scanner
107
+
108
+ [ignore]
109
+ paths = ["tests/fixtures/", "*.generated.py"]
110
+
111
+ [ai]
112
+ enabled = false # optional LLM remediation suggestions
113
+ provider = "gemini" # "anthropic" | "gemini"
114
+ model = "" # blank = provider default
115
+ max_findings = 3
116
+ ```
117
+
118
+ **AI suggestions** are off by default and advisory only — they never affect
119
+ the pass/block decision or modify files, they announce before sending code
120
+ to the API, and secret-bearing files are never sent.
121
+
122
+ | provider | key env var | extra install |
123
+ |---|---|---|
124
+ | `gemini` | `GEMINI_API_KEY` | none (stdlib HTTP) |
125
+ | `anthropic` | `ANTHROPIC_API_KEY` | `pip install ".[ai]"` |
126
+
127
+ ## Overrides
128
+
129
+ - `GIT_SECURITY_NO_BLOCK=1 git commit …` — run the scan, report, never block.
130
+ - `git commit --no-verify` — skip the hook entirely (Git built-in).
131
+
132
+ ## Development
133
+
134
+ ```bash
135
+ pip install -e ".[scanners,dev]"
136
+ pytest
137
+ ruff check src tests && ruff format --check src tests
138
+ ```
139
+
140
+ ## Scope / non-goals
141
+
142
+ No dependency-CVE scanning, no license checks, no SBOM, no IaC/container
143
+ scanning, no non-Python static analysis. The bundled Semgrep ruleset is
144
+ curated and intentionally small — not a replacement for a full SAST platform
145
+ or the Semgrep registry.
146
+
147
+ ## License
148
+
149
+ MIT — see [LICENSE](LICENSE).