git-security-tool 0.1.0__py3-none-any.whl

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 (41) hide show
  1. git_security/__init__.py +3 -0
  2. git_security/__main__.py +5 -0
  3. git_security/baseline.py +67 -0
  4. git_security/cli.py +87 -0
  5. git_security/config/__init__.py +0 -0
  6. git_security/config/loader.py +127 -0
  7. git_security/git/__init__.py +0 -0
  8. git_security/git/diff.py +50 -0
  9. git_security/git/hooks.py +25 -0
  10. git_security/git/repository.py +38 -0
  11. git_security/ignore.py +27 -0
  12. git_security/installer/__init__.py +0 -0
  13. git_security/installer/dependencies.py +14 -0
  14. git_security/installer/git_hook.py +122 -0
  15. git_security/models/__init__.py +0 -0
  16. git_security/models/finding.py +33 -0
  17. git_security/policy/__init__.py +0 -0
  18. git_security/policy/engine.py +38 -0
  19. git_security/reporter/__init__.py +0 -0
  20. git_security/reporter/sarif.py +72 -0
  21. git_security/reporter/terminal.py +46 -0
  22. git_security/rules/__init__.py +0 -0
  23. git_security/rules/semgrep/crypto_tls.yml +37 -0
  24. git_security/rules/semgrep/deserialization.yml +38 -0
  25. git_security/rules/semgrep/filesystem_net.yml +38 -0
  26. git_security/rules/semgrep/injection.yml +43 -0
  27. git_security/rules/semgrep/web.yml +32 -0
  28. git_security/scan.py +245 -0
  29. git_security/scanners/__init__.py +0 -0
  30. git_security/scanners/base.py +34 -0
  31. git_security/scanners/gitleaks.py +59 -0
  32. git_security/scanners/ruff.py +103 -0
  33. git_security/scanners/semgrep.py +81 -0
  34. git_security/suggestions/__init__.py +0 -0
  35. git_security/suggestions/llm.py +67 -0
  36. git_security/suggestions/providers.py +110 -0
  37. git_security_tool-0.1.0.dist-info/METADATA +149 -0
  38. git_security_tool-0.1.0.dist-info/RECORD +41 -0
  39. git_security_tool-0.1.0.dist-info/WHEEL +4 -0
  40. git_security_tool-0.1.0.dist-info/entry_points.txt +2 -0
  41. git_security_tool-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,103 @@
1
+ """Ruff wrapper: staged Python files -> list[Finding].
2
+
3
+ Knows how to talk to Ruff and how to map Ruff's JSON to the normalized
4
+ ``Finding`` model. Knows nothing about Git internals, policy, or reporting.
5
+
6
+ Approach A (milestone 3): scans the files as they exist in the working tree,
7
+ not the exact staged blobs. Editing a file after ``git add`` means Ruff sees
8
+ the newer version. Precise staged-content scanning is a later milestone.
9
+ """
10
+
11
+ import json
12
+ from pathlib import Path
13
+
14
+ from git_security.models.finding import Finding, Severity
15
+ from git_security.scanners.base import run_tool, to_repo_relative
16
+
17
+ _NOT_FOUND_MSG = (
18
+ "[git-security-tool] ruff not found on PATH - skipping "
19
+ "(activate your venv / pip install ruff)"
20
+ )
21
+
22
+
23
+ def run_ruff(files: list[str], repo_root: Path) -> list[Finding]:
24
+ """Lint the Python files in *files* and return normalized findings."""
25
+ python_files = [f for f in files if f.endswith(".py")]
26
+ if not python_files:
27
+ return []
28
+
29
+ proc = run_tool(["ruff", "check", "--output-format=json", "--", *python_files])
30
+ if proc is None:
31
+ print(_NOT_FOUND_MSG)
32
+ return []
33
+
34
+ # Ruff exit codes: 0 = clean, 1 = issues found, 2 = execution error.
35
+ if proc.returncode == 2:
36
+ raise RuntimeError(f"ruff failed: {proc.stderr.strip()}")
37
+
38
+ if not proc.stdout.strip():
39
+ return []
40
+
41
+ return [_to_finding(item, repo_root) for item in json.loads(proc.stdout)]
42
+
43
+
44
+ def _to_finding(item: dict, repo_root: Path) -> Finding:
45
+ location = item.get("location") or {}
46
+ return Finding(
47
+ tool="ruff",
48
+ rule=item.get("code") or "",
49
+ severity=Severity.LOW, # code quality never blocks by default
50
+ file=to_repo_relative(item.get("filename") or "", repo_root),
51
+ line=location.get("row") or 0,
52
+ message=item.get("message") or "",
53
+ )
54
+
55
+
56
+ def run_ruff_format(files: list[str], repo_root: Path) -> list[Finding]:
57
+ """Report Python files that are not formatted (``ruff format --check``)."""
58
+ python_files = [f for f in files if f.endswith(".py")]
59
+ if not python_files:
60
+ return []
61
+
62
+ proc = run_tool(["ruff", "format", "--check", "--", *python_files])
63
+ if proc is None:
64
+ return [] # run_ruff already printed the "not found" message
65
+
66
+ # 0 = all formatted, 1 = some would be reformatted, >1 = execution error.
67
+ if proc.returncode == 0:
68
+ return []
69
+ if proc.returncode not in (0, 1):
70
+ raise RuntimeError(f"ruff format failed: {proc.stderr.strip()}")
71
+
72
+ findings: list[Finding] = []
73
+ seen: set[str] = set()
74
+ for line in f"{proc.stdout}\n{proc.stderr}".splitlines():
75
+ path = _parse_unformatted_path(line)
76
+ if path and path not in seen:
77
+ seen.add(path)
78
+ findings.append(
79
+ Finding(
80
+ tool="ruff",
81
+ rule="format",
82
+ severity=Severity.LOW,
83
+ file=to_repo_relative(path, repo_root),
84
+ line=0,
85
+ message="file is not formatted (run `ruff format`)",
86
+ )
87
+ )
88
+ return findings
89
+
90
+
91
+ def _parse_unformatted_path(line: str) -> str | None:
92
+ """Pull the file path out of a `ruff format --check` output line."""
93
+ line = line.strip()
94
+ if line.startswith("--> "): # newer ruff: " --> path:line:col"
95
+ loc = line[4:]
96
+ head, _, tail = loc.rpartition(":")
97
+ base, _, mid = head.rpartition(":")
98
+ if tail.isdigit() and mid.isdigit() and base:
99
+ return base
100
+ return loc
101
+ if line.startswith("Would reformat: "): # older ruff
102
+ return line[len("Would reformat: ") :]
103
+ return None
@@ -0,0 +1,81 @@
1
+ """Semgrep wrapper: staged files -> list[Finding].
2
+
3
+ Runs Semgrep with our local rule set and maps Semgrep's JSON results to the
4
+ normalized ``Finding`` model.
5
+
6
+ Rule-source decision (milestone 6): we use only the local ``rules/semgrep/``
7
+ directory - fully offline and deterministic, no code or metadata leaves the
8
+ machine. Registry rule packs (``p/python``, ``p/security-audit``) can be
9
+ added as an opt-in config option later.
10
+ """
11
+
12
+ import json
13
+ from pathlib import Path
14
+
15
+ from git_security.models.finding import Finding, Severity
16
+ from git_security.scanners.base import run_tool, to_repo_relative
17
+
18
+ _NOT_FOUND_MSG = (
19
+ "[git-security-tool] semgrep not found on PATH - skipping (pip install semgrep)"
20
+ )
21
+
22
+ # Semgrep severities -> our severity scale.
23
+ _SEVERITY_MAP = {
24
+ "ERROR": Severity.HIGH,
25
+ "WARNING": Severity.MEDIUM,
26
+ "INFO": Severity.LOW,
27
+ }
28
+
29
+
30
+ def run_semgrep(files: list[str], repo_root: Path, rules_dir: Path) -> list[Finding]:
31
+ """Scan *files* with the rules in *rules_dir* and return normalized findings."""
32
+ if not files:
33
+ return []
34
+ if not rules_dir.exists():
35
+ print(f"[git-security-tool] semgrep rules not found at {rules_dir} - skipping")
36
+ return []
37
+
38
+ proc = run_tool(
39
+ [
40
+ "semgrep",
41
+ "--config",
42
+ str(rules_dir),
43
+ "--json",
44
+ "--quiet",
45
+ "--metrics=off",
46
+ "--disable-version-check",
47
+ "--",
48
+ *files,
49
+ ]
50
+ )
51
+ if proc is None:
52
+ print(_NOT_FOUND_MSG)
53
+ return []
54
+
55
+ # Semgrep exit codes: 0 = no findings, 1 = findings found, >=2 = error.
56
+ if proc.returncode not in (0, 1):
57
+ detail = proc.stderr.strip() or proc.stdout.strip()
58
+ raise RuntimeError(f"semgrep failed: {detail}")
59
+
60
+ if not proc.stdout.strip():
61
+ return []
62
+
63
+ data = json.loads(proc.stdout)
64
+ return [_to_finding(r, repo_root) for r in data.get("results", [])]
65
+
66
+
67
+ def _to_finding(result: dict, repo_root: Path) -> Finding:
68
+ extra = result.get("extra") or {}
69
+ start = result.get("start") or {}
70
+ # Semgrep prefixes check_id with the rule file's path
71
+ # (e.g. "src.git_security.rules.semgrep.python-dangerous-eval").
72
+ # Keep only the rule's own id.
73
+ check_id = result.get("check_id") or ""
74
+ return Finding(
75
+ tool="semgrep",
76
+ rule=check_id.rsplit(".", 1)[-1],
77
+ severity=_SEVERITY_MAP.get(extra.get("severity", ""), Severity.MEDIUM),
78
+ file=to_repo_relative(result.get("path") or "", repo_root),
79
+ line=start.get("line") or 0,
80
+ message=(extra.get("message") or "").strip(),
81
+ )
File without changes
@@ -0,0 +1,67 @@
1
+ """Optional AI remediation suggestions - advisory, never changes the block.
2
+
3
+ Runs only when all of these hold:
4
+
5
+ * ``[ai] enabled = true`` in ``.git-security-tool.toml``
6
+ * the configured provider's API key is set (``ANTHROPIC_API_KEY`` /
7
+ ``GEMINI_API_KEY``)
8
+ * for the anthropic provider, the ``anthropic`` package is installed
9
+
10
+ It never modifies files - it prints suggestions for the developer - and it
11
+ always announces before sending code off the machine. Secret-bearing files
12
+ are filtered out before this module is reached (see ``scan.py``).
13
+ """
14
+
15
+ from git_security.config.loader import AIConfig
16
+ from git_security.models.finding import Finding
17
+ from git_security.suggestions.providers import PROVIDERS
18
+
19
+ _PREFIX = "[git-security-tool]"
20
+
21
+ _SYSTEM = (
22
+ "You are a security code-review assistant. You are given one static-analysis "
23
+ "finding and the surrounding code. Reply with exactly two parts: "
24
+ "(1) one sentence naming the risk, then "
25
+ "(2) a minimal safe rewrite of only the affected lines, in a code block. "
26
+ "No preamble, no extra commentary."
27
+ )
28
+
29
+
30
+ def suggestions_available(provider_name: str) -> bool:
31
+ provider = PROVIDERS.get(provider_name)
32
+ return provider is not None and provider.available()
33
+
34
+
35
+ def build_prompt(finding: Finding, snippet: str) -> str:
36
+ return (
37
+ f"Finding: {finding.tool}:{finding.rule} [{finding.severity.name}]\n"
38
+ f"Location: {finding.file}:{finding.line}\n"
39
+ f"Message: {finding.message}\n\n"
40
+ f"Code:\n{snippet}"
41
+ )
42
+
43
+
44
+ def explain(items: list[tuple[Finding, str]], config: AIConfig) -> None:
45
+ """Print a suggestion for each (finding, code snippet) pair."""
46
+ provider = PROVIDERS[config.provider]
47
+ model = config.model or provider.default_model
48
+
49
+ print(
50
+ f"{_PREFIX} sending {len(items)} finding(s) and code context to "
51
+ f"{provider.name} ({model}) for remediation suggestions..."
52
+ )
53
+
54
+ for finding, snippet in items:
55
+ try:
56
+ text = provider.complete(_SYSTEM, build_prompt(finding, snippet), model)
57
+ except RuntimeError as exc:
58
+ print(f"{_PREFIX} suggestion failed for {finding.file}: {exc}")
59
+ continue
60
+
61
+ print(
62
+ f"\n{_PREFIX} suggestion for {finding.file}:{finding.line} "
63
+ f"({finding.tool}:{finding.rule}):"
64
+ )
65
+ for line in text.strip().splitlines():
66
+ print(f" {line}")
67
+ print()
@@ -0,0 +1,110 @@
1
+ """LLM providers for remediation suggestions.
2
+
3
+ Each provider exposes the same tiny surface:
4
+
5
+ * ``name`` - identifier used in ``[ai] provider`` and in messages
6
+ * ``default_model`` - used when ``[ai] model`` is left blank
7
+ * ``env_var`` - the API-key environment variable
8
+ * ``available()`` - key present (and any SDK importable)
9
+ * ``complete(system, user, model) -> str`` - one call, text back,
10
+ ``RuntimeError`` on any failure
11
+
12
+ This layer is advisory only. The deterministic scanners decide whether a
13
+ commit is blocked; providers just explain findings.
14
+ """
15
+
16
+ import json
17
+ import os
18
+ import urllib.error
19
+ import urllib.request
20
+
21
+ _TIMEOUT = 30
22
+ _MAX_OUTPUT_TOKENS = 2048
23
+
24
+
25
+ class AnthropicProvider:
26
+ name = "anthropic"
27
+ default_model = "claude-opus-5"
28
+ env_var = "ANTHROPIC_API_KEY"
29
+
30
+ def available(self) -> bool:
31
+ if not os.environ.get(self.env_var):
32
+ return False
33
+ try:
34
+ import anthropic # noqa: F401
35
+ except ImportError:
36
+ return False
37
+ return True
38
+
39
+ def complete(self, system: str, user: str, model: str) -> str:
40
+ import anthropic
41
+
42
+ try:
43
+ client = anthropic.Anthropic()
44
+ response = client.messages.create(
45
+ model=model or self.default_model,
46
+ max_tokens=_MAX_OUTPUT_TOKENS,
47
+ system=system,
48
+ messages=[{"role": "user", "content": user}],
49
+ )
50
+ except anthropic.AnthropicError as exc:
51
+ raise RuntimeError(f"Anthropic API error: {exc}") from exc
52
+ return "".join(b.text for b in response.content if b.type == "text")
53
+
54
+
55
+ class GeminiProvider:
56
+ name = "gemini"
57
+ default_model = "gemini-2.5-flash"
58
+ env_var = "GEMINI_API_KEY"
59
+ _URL = (
60
+ "https://generativelanguage.googleapis.com/v1beta/models/"
61
+ "{model}:generateContent"
62
+ )
63
+
64
+ def available(self) -> bool:
65
+ return bool(os.environ.get(self.env_var))
66
+
67
+ def complete(self, system: str, user: str, model: str) -> str:
68
+ url = self._URL.format(model=model or self.default_model)
69
+ body = json.dumps(
70
+ {
71
+ "systemInstruction": {"parts": [{"text": system}]},
72
+ "contents": [{"parts": [{"text": user}]}],
73
+ "generationConfig": {"maxOutputTokens": _MAX_OUTPUT_TOKENS},
74
+ }
75
+ ).encode()
76
+ request = urllib.request.Request( # noqa: S310 - hardcoded https host
77
+ url,
78
+ data=body,
79
+ headers={
80
+ "Content-Type": "application/json",
81
+ "x-goog-api-key": os.environ[self.env_var],
82
+ },
83
+ method="POST",
84
+ )
85
+ try:
86
+ with urllib.request.urlopen(request, timeout=_TIMEOUT) as resp: # noqa: S310
87
+ payload = json.load(resp)
88
+ except urllib.error.HTTPError as exc:
89
+ detail = exc.read().decode("utf-8", "replace")[:300]
90
+ raise RuntimeError(f"Gemini API error {exc.code}: {detail}") from exc
91
+ except (urllib.error.URLError, TimeoutError) as exc:
92
+ raise RuntimeError(f"Gemini request failed: {exc}") from exc
93
+
94
+ candidates = payload.get("candidates") or []
95
+ if not candidates:
96
+ reason = payload.get("promptFeedback", {}).get(
97
+ "blockReason", "no candidates returned"
98
+ )
99
+ raise RuntimeError(f"Gemini returned nothing ({reason})")
100
+ parts = candidates[0].get("content", {}).get("parts") or []
101
+ text = "".join(p.get("text", "") for p in parts)
102
+ if not text:
103
+ raise RuntimeError(
104
+ "Gemini response had no text "
105
+ f"(finishReason={candidates[0].get('finishReason')})"
106
+ )
107
+ return text
108
+
109
+
110
+ PROVIDERS = {p.name: p for p in (AnthropicProvider(), GeminiProvider())}
@@ -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).
@@ -0,0 +1,41 @@
1
+ git_security/__init__.py,sha256=9Ib4IpixasuGfmRXPglAU8a7bztRuL2xldeiBi0beYY,92
2
+ git_security/__main__.py,sha256=67q1VsxrDi8_JYkHoBXVJ2NXsDEoiu58Qb4LLU8zhys,100
3
+ git_security/baseline.py,sha256=WwmkzXrq8wUtBHW0h502Z16kPDhK2Nh-9M8DQlD0h0Y,2216
4
+ git_security/cli.py,sha256=SyV3VTK6nUxxCRl4fDmGCyIWSumkZR4ny5K_PhFI7QI,2745
5
+ git_security/ignore.py,sha256=0ORE9hsX3aZTxGSRrXE7Kp2tadFa9mU7-Aawbi2FDFc,882
6
+ git_security/scan.py,sha256=MUk08tkuKYKuAVEDycn-Fe40CxUNIpHUywx16WK7p6o,8870
7
+ git_security/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ git_security/config/loader.py,sha256=mDJOajILb5jS8HA0YeCDu4jEYWMcXfKr5gEOEnCoMc4,4152
9
+ git_security/git/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ git_security/git/diff.py,sha256=filp69ZDfaLJzqAuj-_Tot4YSKs9Z3_XkbG-8fMzK-o,1826
11
+ git_security/git/hooks.py,sha256=3ELh7MU5Zp9fQ96wmXEHn7YfNYA3Oh7-FPNPFq7DM0k,808
12
+ git_security/git/repository.py,sha256=FofI9V0T0MV4z9YxnYSQVlGt5f-FIaqnER4dO-vormg,1152
13
+ git_security/installer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ git_security/installer/dependencies.py,sha256=XJXsAAx3QYY0yZxSPV9kVT2k9vSl7wkVSiLa2UgHwug,453
15
+ git_security/installer/git_hook.py,sha256=LJlG10PW0lmQO4SDESgpnIULF-Z5jGVH2XNq703jNoo,3493
16
+ git_security/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ git_security/models/finding.py,sha256=K05V8_0xIP22RsIvnMnQA8vQxExZK3grJ1VVYQBvP9A,935
18
+ git_security/policy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ git_security/policy/engine.py,sha256=SReP9g9BaDB3tCue3mhLhjolHVX4pvVEU2eNJchx05g,1278
20
+ git_security/reporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ git_security/reporter/sarif.py,sha256=vAq4Z-MFwzm1VgHma3_zotvm04LRIEARzX7YbNEgE2U,2088
22
+ git_security/reporter/terminal.py,sha256=5tyczZHGDEsM3uKVB_YRwvprmpsd0kirOY2WWSLl8is,1545
23
+ git_security/rules/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
+ git_security/rules/semgrep/crypto_tls.yml,sha256=q8CzV3TdVnq1vgOWn1bdmXftfcw-yW571KQookimN_c,1408
25
+ git_security/rules/semgrep/deserialization.yml,sha256=ux1ZmYP1dbKLw_uddAyYDx90jLmNOLbd6ARohUuNi20,1450
26
+ git_security/rules/semgrep/filesystem_net.yml,sha256=5-bKkebIABTjEtyeCIHC9Md-tVefhnREw4DDcoGuSbI,1348
27
+ git_security/rules/semgrep/injection.yml,sha256=I5e0fzltTbnxQBhhqCYpC64daE-85cr1hmRrzjAnrik,1393
28
+ git_security/rules/semgrep/web.yml,sha256=v47KwR3JCUyD0Jpy6bpVkZY8VOA7pVPyTa4RutVZY5g,1194
29
+ git_security/scanners/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ git_security/scanners/base.py,sha256=DbJAd1eS7ruXReLezgmJ8zjSSTkSftUAFXWetVWlOWw,1119
31
+ git_security/scanners/gitleaks.py,sha256=3tByuHxBVLtlj2ceikYe9Q2TcYffb46RffSQGRilt78,1869
32
+ git_security/scanners/ruff.py,sha256=SDMeTevpZ5I50PTkaeDpVFbuwDXJhpuHKtYbm1HUnaQ,3652
33
+ git_security/scanners/semgrep.py,sha256=zV4-HeVArjx3LgS12SMpEk4pqlHCXF-_yxNjWu9kpiw,2603
34
+ git_security/suggestions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ git_security/suggestions/llm.py,sha256=0-KOjgHFcfgGENK--8JQBRk7ACiY-IZVu9xM-k1am_E,2409
36
+ git_security/suggestions/providers.py,sha256=ku7G6Iv0fV2ScsN1OC4kNSpnvu77OsgPAZ_gQeQVSyk,3830
37
+ git_security_tool-0.1.0.dist-info/METADATA,sha256=vneuYGLJ3vXQCcMrLF2WRySJZw35cSp0CZkZQDgLbRI,5569
38
+ git_security_tool-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
39
+ git_security_tool-0.1.0.dist-info/entry_points.txt,sha256=NXfJ9zp9fFPnybQF1HcwP8msR7qXy6GbAeqAS6eBmbQ,60
40
+ git_security_tool-0.1.0.dist-info/licenses/LICENSE,sha256=ymE5r69euauSBB_4QN1rLQuZ5yEOveh0Ctx3z5SCzw4,1079
41
+ git_security_tool-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ git-security-tool = git_security.cli:main
@@ -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.