gitrupt 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 (48) hide show
  1. gitrupt/__init__.py +13 -0
  2. gitrupt/cli.py +546 -0
  3. gitrupt/config.py +269 -0
  4. gitrupt/git.py +590 -0
  5. gitrupt/hooks/__init__.py +7 -0
  6. gitrupt/hooks/install.py +255 -0
  7. gitrupt/hooks/pre_commit.py +103 -0
  8. gitrupt/hooks/pre_push.py +178 -0
  9. gitrupt/models.py +190 -0
  10. gitrupt/policy.py +36 -0
  11. gitrupt/reporting.py +316 -0
  12. gitrupt/risk.py +197 -0
  13. gitrupt/scanner.py +117 -0
  14. gitrupt/scanners/__init__.py +17 -0
  15. gitrupt/scanners/adapters.py +166 -0
  16. gitrupt/scanners/base.py +113 -0
  17. gitrupt/scanners/binaries.py +185 -0
  18. gitrupt/scanners/code_rules/__init__.py +36 -0
  19. gitrupt/scanners/code_rules/base.py +27 -0
  20. gitrupt/scanners/code_rules/go.py +65 -0
  21. gitrupt/scanners/code_rules/javascript.py +106 -0
  22. gitrupt/scanners/code_rules/php.py +71 -0
  23. gitrupt/scanners/code_rules/powershell.py +85 -0
  24. gitrupt/scanners/code_rules/python.py +153 -0
  25. gitrupt/scanners/code_rules/ruby.py +76 -0
  26. gitrupt/scanners/code_rules/rust.py +41 -0
  27. gitrupt/scanners/code_rules/shell.py +112 -0
  28. gitrupt/scanners/dependencies.py +244 -0
  29. gitrupt/scanners/ecosystems/__init__.py +30 -0
  30. gitrupt/scanners/ecosystems/base.py +60 -0
  31. gitrupt/scanners/ecosystems/node.py +128 -0
  32. gitrupt/scanners/ecosystems/python.py +157 -0
  33. gitrupt/scanners/entropy.py +123 -0
  34. gitrupt/scanners/forbidden_files.py +201 -0
  35. gitrupt/scanners/malware.py +219 -0
  36. gitrupt/scanners/osv_client.py +221 -0
  37. gitrupt/scanners/registry.py +66 -0
  38. gitrupt/scanners/secret_rules.py +368 -0
  39. gitrupt/scanners/secrets.py +558 -0
  40. gitrupt/scanners/suspicious_code.py +208 -0
  41. gitrupt/scanners/yara_loader.py +65 -0
  42. gitrupt/scanners/yara_rules_builtin.py +141 -0
  43. gitrupt-0.1.0.dist-info/METADATA +342 -0
  44. gitrupt-0.1.0.dist-info/RECORD +48 -0
  45. gitrupt-0.1.0.dist-info/WHEEL +5 -0
  46. gitrupt-0.1.0.dist-info/entry_points.txt +2 -0
  47. gitrupt-0.1.0.dist-info/licenses/LICENSE +23 -0
  48. gitrupt-0.1.0.dist-info/top_level.txt +1 -0
gitrupt/config.py ADDED
@@ -0,0 +1,269 @@
1
+ """
2
+ Configuration management for Gitrupt.
3
+
4
+ Reads .gitrupt.yml from the repository root and provides
5
+ a validated, typed configuration object.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ import yaml
15
+ from pydantic import BaseModel, Field, field_validator, model_validator
16
+
17
+ from gitrupt.models import PolicyAction, Severity
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ # Default configuration values
22
+ DEFAULT_FORBIDDEN_PATHS: list[str] = [
23
+ ".env",
24
+ ".env.*",
25
+ "*.pem",
26
+ "*.key",
27
+ "*.p12",
28
+ "*.pfx",
29
+ "credentials.json",
30
+ "service-account.json",
31
+ "*.secret",
32
+ ]
33
+
34
+ DEFAULT_FORBIDDEN_EXTENSIONS: list[str] = [
35
+ ".scr",
36
+ ]
37
+
38
+ DEFAULT_ALLOW_PATHS: list[str] = [
39
+ ".env.example",
40
+ ]
41
+
42
+ CONFIG_FILENAME = ".gitrupt.yml"
43
+
44
+
45
+ class ScanConfig(BaseModel):
46
+ """Which scanners to run."""
47
+
48
+ secrets: bool = True
49
+ threats: bool = True
50
+ suspicious_code: bool = False
51
+ dependencies: bool = False
52
+
53
+ model_config = {"frozen": True}
54
+
55
+
56
+
57
+ class EntropyConfig(BaseModel):
58
+ enabled: bool = True
59
+ threshold: float = Field(default=4.5, ge=0.0, le=8.0)
60
+ min_length: int = Field(default=20, ge=8)
61
+
62
+ model_config = {"frozen": True}
63
+
64
+
65
+
66
+
67
+ class RulesConfig(BaseModel):
68
+ """File and path rules."""
69
+
70
+ forbidden_paths: list[str] = Field(default_factory=lambda: list(DEFAULT_FORBIDDEN_PATHS))
71
+ forbidden_extensions: list[str] = Field(
72
+ default_factory=lambda: list(DEFAULT_FORBIDDEN_EXTENSIONS)
73
+ )
74
+ max_file_size_mb: float = 50.0
75
+
76
+ model_config = {"frozen": True}
77
+
78
+
79
+ class AllowConfig(BaseModel):
80
+ """Allowlisted paths that bypass rules."""
81
+
82
+ paths: list[str] = Field(default_factory=lambda: list(DEFAULT_ALLOW_PATHS))
83
+
84
+ model_config = {"frozen": True}
85
+
86
+
87
+ class SuppressionConfig(BaseModel):
88
+ """Suppression and allowlist settings for findings."""
89
+
90
+ rules: list[str] = Field(default_factory=list)
91
+ files: list[str] = Field(default_factory=list)
92
+ paths: list[str] = Field(default_factory=list)
93
+
94
+ model_config = {"frozen": True}
95
+
96
+
97
+ class PolicyConfig(BaseModel):
98
+ """Severity → action mapping."""
99
+
100
+ low: PolicyAction = PolicyAction.ALLOW
101
+ medium: PolicyAction = PolicyAction.WARN
102
+ high: PolicyAction = PolicyAction.BLOCK
103
+ critical: PolicyAction = PolicyAction.BLOCK
104
+
105
+ model_config = {"frozen": True}
106
+
107
+ def action_for(self, severity: Severity) -> PolicyAction:
108
+ return {
109
+ Severity.LOW: self.low,
110
+ Severity.MEDIUM: self.medium,
111
+ Severity.HIGH: self.high,
112
+ Severity.CRITICAL: self.critical,
113
+ }[severity]
114
+
115
+
116
+ class SuspiciousCodeConfig(BaseModel):
117
+ """Tuning for the native suspicious-code scanner."""
118
+
119
+ min_confidence: float = Field(default=0.6, ge=0.0, le=1.0)
120
+ languages: list[str] = Field(
121
+ default_factory=lambda: [
122
+ "python",
123
+ "javascript",
124
+ "shell",
125
+ "powershell",
126
+ "php",
127
+ "ruby",
128
+ "go",
129
+ ]
130
+ )
131
+
132
+ model_config = {"frozen": True}
133
+
134
+
135
+
136
+
137
+
138
+ class MalwareConfig(BaseModel):
139
+ """Tuning for the native malware scanner."""
140
+
141
+ use_yara: bool = True
142
+ max_file_size_mb: float = Field(default=50.0, gt=0.0)
143
+ custom_yara_dirs: list[str] = Field(default_factory=list)
144
+
145
+ model_config = {"frozen": True}
146
+
147
+
148
+
149
+
150
+ class DependenciesConfig(BaseModel):
151
+ """Tuning for the native dependency scanner."""
152
+
153
+ ecosystems: list[str] = Field(default_factory=lambda: ["python", "node"])
154
+ offline: bool = False
155
+ timeout_seconds: float = Field(default=10.0, gt=0.0)
156
+ cache_ttl_hours: int = Field(default=24, ge=0)
157
+
158
+ model_config = {"frozen": True}
159
+
160
+ class HooksConfig(BaseModel):
161
+ """Which Git hooks Gitrupt installs and honors."""
162
+
163
+ pre_commit: bool = True
164
+ pre_push: bool = True
165
+
166
+ model_config = {"frozen": True}
167
+
168
+
169
+
170
+
171
+ class GitruptConfig(BaseModel):
172
+ """
173
+ Top-level Gitrupt configuration.
174
+
175
+ Read from .gitrupt.yml in the repository root.
176
+ Falls back to safe defaults if no file is present.
177
+ """
178
+
179
+ version: int = 1
180
+ mode: str = "strict"
181
+ scan: ScanConfig = Field(default_factory=ScanConfig)
182
+ rules: RulesConfig = Field(default_factory=RulesConfig)
183
+ allow: AllowConfig = Field(default_factory=AllowConfig)
184
+ suppressions: SuppressionConfig = Field(default_factory=SuppressionConfig)
185
+ suspicious_code: SuspiciousCodeConfig = Field(default_factory=SuspiciousCodeConfig)
186
+ malware: MalwareConfig = Field(default_factory=MalwareConfig)
187
+ dependencies: DependenciesConfig = Field(default_factory=DependenciesConfig)
188
+ hooks: HooksConfig = Field(default_factory=HooksConfig)
189
+ entropy: EntropyConfig = Field(default_factory=EntropyConfig)
190
+ policy: PolicyConfig = Field(default_factory=PolicyConfig)
191
+ model_config = {"frozen": True}
192
+
193
+ @field_validator("version")
194
+ @classmethod
195
+ def validate_version(cls, v: int) -> int:
196
+ if v != 1:
197
+ raise ValueError(f"Unsupported configuration version: {v}. Only version 1 is supported.")
198
+ return v
199
+
200
+ @field_validator("mode")
201
+ @classmethod
202
+ def validate_mode(cls, v: str) -> str:
203
+ valid_modes = {"strict", "permissive", "warn-only"}
204
+ if v not in valid_modes:
205
+ raise ValueError(f"Invalid mode '{v}'. Must be one of: {', '.join(valid_modes)}")
206
+ return v
207
+
208
+ @model_validator(mode="after")
209
+ def apply_mode_overrides(self) -> "GitruptConfig":
210
+ """In warn-only mode, override all blocking policies to warn."""
211
+ if self.mode == "warn-only":
212
+ object.__setattr__(
213
+ self,
214
+ "policy",
215
+ PolicyConfig(
216
+ low=PolicyAction.ALLOW,
217
+ medium=PolicyAction.WARN,
218
+ high=PolicyAction.WARN,
219
+ critical=PolicyAction.WARN,
220
+ ),
221
+ )
222
+ return self
223
+
224
+
225
+ def load_config(repo_root: str | Path) -> GitruptConfig:
226
+ """
227
+ Load Gitrupt configuration from the repository root.
228
+
229
+ If no .gitrupt.yml exists, returns a safe default configuration.
230
+ If the file is malformed, raises a descriptive error.
231
+ """
232
+ config_path = Path(repo_root) / CONFIG_FILENAME
233
+
234
+ if not config_path.exists():
235
+ logger.debug("No .gitrupt.yml found, using defaults")
236
+ return GitruptConfig()
237
+
238
+ try:
239
+ with config_path.open("r", encoding="utf-8") as f:
240
+ raw: dict[str, Any] = yaml.safe_load(f) or {}
241
+ except yaml.YAMLError as e:
242
+ raise ConfigurationError(f"Invalid YAML in {config_path}: {e}") from e
243
+ except OSError as e:
244
+ raise ConfigurationError(f"Cannot read {config_path}: {e}") from e
245
+
246
+ try:
247
+ config = GitruptConfig.model_validate(raw)
248
+ except Exception as e:
249
+ raise ConfigurationError(f"Invalid configuration in {config_path}: {e}") from e
250
+
251
+ logger.debug("Loaded configuration from %s", config_path)
252
+ return config
253
+
254
+
255
+ def find_config_path(start: str | Path) -> Path | None:
256
+ """Walk up from start looking for .gitrupt.yml."""
257
+ current = Path(start).resolve()
258
+ for directory in [current, *current.parents]:
259
+ candidate = directory / CONFIG_FILENAME
260
+ if candidate.exists():
261
+ return candidate
262
+ # Stop at filesystem root
263
+ if directory == directory.parent:
264
+ break
265
+ return None
266
+
267
+
268
+ class ConfigurationError(Exception):
269
+ """Raised when the Gitrupt configuration is invalid."""