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.
- gitrupt/__init__.py +13 -0
- gitrupt/cli.py +546 -0
- gitrupt/config.py +269 -0
- gitrupt/git.py +590 -0
- gitrupt/hooks/__init__.py +7 -0
- gitrupt/hooks/install.py +255 -0
- gitrupt/hooks/pre_commit.py +103 -0
- gitrupt/hooks/pre_push.py +178 -0
- gitrupt/models.py +190 -0
- gitrupt/policy.py +36 -0
- gitrupt/reporting.py +316 -0
- gitrupt/risk.py +197 -0
- gitrupt/scanner.py +117 -0
- gitrupt/scanners/__init__.py +17 -0
- gitrupt/scanners/adapters.py +166 -0
- gitrupt/scanners/base.py +113 -0
- gitrupt/scanners/binaries.py +185 -0
- gitrupt/scanners/code_rules/__init__.py +36 -0
- gitrupt/scanners/code_rules/base.py +27 -0
- gitrupt/scanners/code_rules/go.py +65 -0
- gitrupt/scanners/code_rules/javascript.py +106 -0
- gitrupt/scanners/code_rules/php.py +71 -0
- gitrupt/scanners/code_rules/powershell.py +85 -0
- gitrupt/scanners/code_rules/python.py +153 -0
- gitrupt/scanners/code_rules/ruby.py +76 -0
- gitrupt/scanners/code_rules/rust.py +41 -0
- gitrupt/scanners/code_rules/shell.py +112 -0
- gitrupt/scanners/dependencies.py +244 -0
- gitrupt/scanners/ecosystems/__init__.py +30 -0
- gitrupt/scanners/ecosystems/base.py +60 -0
- gitrupt/scanners/ecosystems/node.py +128 -0
- gitrupt/scanners/ecosystems/python.py +157 -0
- gitrupt/scanners/entropy.py +123 -0
- gitrupt/scanners/forbidden_files.py +201 -0
- gitrupt/scanners/malware.py +219 -0
- gitrupt/scanners/osv_client.py +221 -0
- gitrupt/scanners/registry.py +66 -0
- gitrupt/scanners/secret_rules.py +368 -0
- gitrupt/scanners/secrets.py +558 -0
- gitrupt/scanners/suspicious_code.py +208 -0
- gitrupt/scanners/yara_loader.py +65 -0
- gitrupt/scanners/yara_rules_builtin.py +141 -0
- gitrupt-0.1.0.dist-info/METADATA +342 -0
- gitrupt-0.1.0.dist-info/RECORD +48 -0
- gitrupt-0.1.0.dist-info/WHEEL +5 -0
- gitrupt-0.1.0.dist-info/entry_points.txt +2 -0
- gitrupt-0.1.0.dist-info/licenses/LICENSE +23 -0
- gitrupt-0.1.0.dist-info/top_level.txt +1 -0
gitrupt/models.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data models for Gitrupt.
|
|
3
|
+
|
|
4
|
+
All scanners return Finding objects.
|
|
5
|
+
The Risk Engine consumes Finding objects and produces a PolicyDecision.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import uuid
|
|
11
|
+
from enum import Enum
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel, Field
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Severity(str, Enum):
|
|
18
|
+
"""Finding severity level."""
|
|
19
|
+
|
|
20
|
+
LOW = "low"
|
|
21
|
+
MEDIUM = "medium"
|
|
22
|
+
HIGH = "high"
|
|
23
|
+
CRITICAL = "critical"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def score(self) -> int:
|
|
27
|
+
"""Numeric score for this severity level."""
|
|
28
|
+
return {
|
|
29
|
+
Severity.LOW: 20,
|
|
30
|
+
Severity.MEDIUM: 50,
|
|
31
|
+
Severity.HIGH: 70,
|
|
32
|
+
Severity.CRITICAL: 90,
|
|
33
|
+
}[self]
|
|
34
|
+
|
|
35
|
+
def __lt__(self, other: str) -> bool:
|
|
36
|
+
if not isinstance(other, Severity):
|
|
37
|
+
return NotImplemented
|
|
38
|
+
order = [Severity.LOW, Severity.MEDIUM, Severity.HIGH, Severity.CRITICAL]
|
|
39
|
+
return order.index(self) < order.index(other)
|
|
40
|
+
|
|
41
|
+
def __le__(self, other: str) -> bool:
|
|
42
|
+
if not isinstance(other, Severity):
|
|
43
|
+
return NotImplemented
|
|
44
|
+
return self == other or self < other
|
|
45
|
+
|
|
46
|
+
def __gt__(self, other: str) -> bool:
|
|
47
|
+
if not isinstance(other, Severity):
|
|
48
|
+
return NotImplemented
|
|
49
|
+
order = [Severity.LOW, Severity.MEDIUM, Severity.HIGH, Severity.CRITICAL]
|
|
50
|
+
return order.index(self) > order.index(other)
|
|
51
|
+
|
|
52
|
+
def __ge__(self, other: str) -> bool:
|
|
53
|
+
if not isinstance(other, Severity):
|
|
54
|
+
return NotImplemented
|
|
55
|
+
return self == other or self > other
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PolicyAction(str, Enum):
|
|
59
|
+
"""Action to take when a finding is at a given severity level."""
|
|
60
|
+
|
|
61
|
+
ALLOW = "allow"
|
|
62
|
+
WARN = "warn"
|
|
63
|
+
BLOCK = "block"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Finding(BaseModel):
|
|
67
|
+
"""
|
|
68
|
+
A security finding from any scanner.
|
|
69
|
+
|
|
70
|
+
Scanners return Finding objects; they do not make blocking decisions.
|
|
71
|
+
The Risk Engine + Policy Engine make the blocking decision.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8].upper())
|
|
75
|
+
scanner: str
|
|
76
|
+
rule_id: str
|
|
77
|
+
severity: Severity
|
|
78
|
+
confidence: float = Field(ge=0.0, le=1.0)
|
|
79
|
+
file: str
|
|
80
|
+
line: int | None = None
|
|
81
|
+
message: str
|
|
82
|
+
description: str = ""
|
|
83
|
+
evidence: str = "" # Always redacted — never the raw secret
|
|
84
|
+
recommendation: str = ""
|
|
85
|
+
can_override: bool = True
|
|
86
|
+
|
|
87
|
+
model_config = {"frozen": True}
|
|
88
|
+
|
|
89
|
+
def __hash__(self) -> int:
|
|
90
|
+
return hash((self.scanner, self.rule_id, self.file, self.line))
|
|
91
|
+
|
|
92
|
+
def __eq__(self, other: Any) -> bool:
|
|
93
|
+
if not isinstance(other, Finding):
|
|
94
|
+
return NotImplemented
|
|
95
|
+
return (
|
|
96
|
+
self.scanner == other.scanner
|
|
97
|
+
and self.rule_id == other.rule_id
|
|
98
|
+
and self.file == other.file
|
|
99
|
+
and self.line == other.line
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class StagedFile(BaseModel):
|
|
104
|
+
"""A file that is staged for commit."""
|
|
105
|
+
|
|
106
|
+
path: str
|
|
107
|
+
status: str # A=added, M=modified, D=deleted, R=renamed, C=copied
|
|
108
|
+
old_path: str | None = None # Only for renames
|
|
109
|
+
is_binary: bool = False
|
|
110
|
+
size_bytes: int = 0
|
|
111
|
+
|
|
112
|
+
model_config = {"frozen": True}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ScanTarget(BaseModel):
|
|
116
|
+
"""The target of a scan: staged files and their content."""
|
|
117
|
+
|
|
118
|
+
staged_files: list[StagedFile] = Field(default_factory=list)
|
|
119
|
+
staged_diff: str = ""
|
|
120
|
+
repo_root: str = ""
|
|
121
|
+
|
|
122
|
+
model_config = {"frozen": False}
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def file_paths(self) -> list[str]:
|
|
126
|
+
return [f.path for f in self.staged_files]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class ScanResult(BaseModel):
|
|
130
|
+
"""The aggregated result of running all scanners."""
|
|
131
|
+
|
|
132
|
+
findings: list[Finding] = Field(default_factory=list)
|
|
133
|
+
files_scanned: int = 0
|
|
134
|
+
scan_duration_ms: float = 0.0
|
|
135
|
+
scanners_run: list[str] = Field(default_factory=list)
|
|
136
|
+
|
|
137
|
+
model_config = {"frozen": False}
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def has_findings(self) -> bool:
|
|
141
|
+
return len(self.findings) > 0
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def critical_findings(self) -> list[Finding]:
|
|
145
|
+
return [f for f in self.findings if f.severity == Severity.CRITICAL]
|
|
146
|
+
|
|
147
|
+
@property
|
|
148
|
+
def high_findings(self) -> list[Finding]:
|
|
149
|
+
return [f for f in self.findings if f.severity == Severity.HIGH]
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def medium_findings(self) -> list[Finding]:
|
|
153
|
+
return [f for f in self.findings if f.severity == Severity.MEDIUM]
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def low_findings(self) -> list[Finding]:
|
|
157
|
+
return [f for f in self.findings if f.severity == Severity.LOW]
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def max_severity(self) -> Severity | None:
|
|
161
|
+
if not self.findings:
|
|
162
|
+
return None
|
|
163
|
+
return max(f.severity for f in self.findings)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class PolicyDecision(BaseModel):
|
|
167
|
+
"""The final policy decision after applying rules to scan results."""
|
|
168
|
+
|
|
169
|
+
action: PolicyAction
|
|
170
|
+
scan_result: ScanResult
|
|
171
|
+
blocking_findings: list[Finding] = Field(default_factory=list)
|
|
172
|
+
warning_findings: list[Finding] = Field(default_factory=list)
|
|
173
|
+
|
|
174
|
+
model_config = {"frozen": True}
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def blocked(self) -> bool:
|
|
178
|
+
return self.action == PolicyAction.BLOCK
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def allowed(self) -> bool:
|
|
182
|
+
return self.action == PolicyAction.ALLOW
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def has_warnings(self) -> bool:
|
|
186
|
+
return len(self.warning_findings) > 0
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def exit_code(self) -> int:
|
|
190
|
+
return 1 if self.blocked else 0
|
gitrupt/policy.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Policy engine for Gitrupt.
|
|
3
|
+
|
|
4
|
+
Converts configuration into policy decisions.
|
|
5
|
+
Thin wrapper that makes policy intent explicit.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from gitrupt.config import PolicyConfig
|
|
11
|
+
from gitrupt.models import Finding, PolicyAction, Severity
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_action_for_finding(finding: Finding, policy: PolicyConfig) -> PolicyAction:
|
|
15
|
+
"""Return the policy action for a given finding."""
|
|
16
|
+
return policy.action_for(finding.severity)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_blocking(finding: Finding, policy: PolicyConfig) -> bool:
|
|
20
|
+
"""Return True if this finding should block a commit."""
|
|
21
|
+
return get_action_for_finding(finding, policy) == PolicyAction.BLOCK
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_warning(finding: Finding, policy: PolicyConfig) -> bool:
|
|
25
|
+
"""Return True if this finding should produce a warning (but not block)."""
|
|
26
|
+
return get_action_for_finding(finding, policy) == PolicyAction.WARN
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_severity_label(severity: Severity) -> str:
|
|
30
|
+
"""Return a human-readable label for a severity level."""
|
|
31
|
+
return {
|
|
32
|
+
Severity.LOW: "Low",
|
|
33
|
+
Severity.MEDIUM: "Medium",
|
|
34
|
+
Severity.HIGH: "High",
|
|
35
|
+
Severity.CRITICAL: "Critical",
|
|
36
|
+
}[severity]
|
gitrupt/reporting.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Terminal reporter for Gitrupt.
|
|
3
|
+
|
|
4
|
+
Produces human-readable, colored output for the developer.
|
|
5
|
+
|
|
6
|
+
Security rule: secrets are NEVER printed in full.
|
|
7
|
+
All evidence fields are already redacted before reaching this module.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich import box
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
|
|
21
|
+
from gitrupt.models import Finding, PolicyDecision, PolicyAction, ScanResult, Severity
|
|
22
|
+
|
|
23
|
+
# Force UTF-8 output on Windows to avoid cp1252 encoding errors with special chars
|
|
24
|
+
if sys.platform == "win32":
|
|
25
|
+
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
|
26
|
+
|
|
27
|
+
# Create console instances — one for stdout, one for stderr
|
|
28
|
+
# force_terminal=True ensures colored output; highlight=False avoids auto-detection issues
|
|
29
|
+
console = Console(force_terminal=True, highlight=False)
|
|
30
|
+
error_console = Console(stderr=True, force_terminal=True, highlight=False)
|
|
31
|
+
|
|
32
|
+
# Severity color map
|
|
33
|
+
SEVERITY_COLORS = {
|
|
34
|
+
Severity.CRITICAL: "bold red",
|
|
35
|
+
Severity.HIGH: "red",
|
|
36
|
+
Severity.MEDIUM: "yellow",
|
|
37
|
+
Severity.LOW: "cyan",
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
SEVERITY_ICONS = {
|
|
41
|
+
Severity.CRITICAL: "[CRIT]",
|
|
42
|
+
Severity.HIGH: "[HIGH]",
|
|
43
|
+
Severity.MEDIUM: "[MED] ",
|
|
44
|
+
Severity.LOW: "[LOW] ",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def print_header() -> None:
|
|
49
|
+
"""Print the Gitrupt header."""
|
|
50
|
+
console.print()
|
|
51
|
+
console.print("[bold blue][Gitrupt][/bold blue]")
|
|
52
|
+
console.print()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def print_scanning(file_count: int) -> None:
|
|
56
|
+
"""Print the 'scanning...' status."""
|
|
57
|
+
console.print(f"[dim]Scanning {file_count} staged file{'s' if file_count != 1 else ''}...[/dim]")
|
|
58
|
+
console.print()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def print_decision(decision: PolicyDecision) -> None:
|
|
62
|
+
"""Print the full scan result and policy decision."""
|
|
63
|
+
scan = decision.scan_result
|
|
64
|
+
|
|
65
|
+
if decision.blocked:
|
|
66
|
+
_print_blocked(decision)
|
|
67
|
+
elif decision.has_warnings:
|
|
68
|
+
_print_warnings(decision)
|
|
69
|
+
else:
|
|
70
|
+
_print_pass(scan)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def print_scan_stats(scan: ScanResult) -> None:
|
|
74
|
+
"""Print scan statistics."""
|
|
75
|
+
duration = scan.scan_duration_ms
|
|
76
|
+
console.print(
|
|
77
|
+
f"[dim]Scanned {scan.files_scanned} file{'s' if scan.files_scanned != 1 else ''} "
|
|
78
|
+
f"in {duration:.0f}ms[/dim]"
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _print_blocked(decision: PolicyDecision) -> None:
|
|
83
|
+
"""Print a BLOCKED result."""
|
|
84
|
+
error_console.print(
|
|
85
|
+
Panel(
|
|
86
|
+
"[bold red]COMMIT BLOCKED[/bold red]",
|
|
87
|
+
border_style="red",
|
|
88
|
+
expand=False,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
error_console.print()
|
|
92
|
+
|
|
93
|
+
all_findings = decision.blocking_findings + decision.warning_findings
|
|
94
|
+
|
|
95
|
+
for finding in all_findings:
|
|
96
|
+
_print_finding(finding, is_blocking=finding in decision.blocking_findings)
|
|
97
|
+
|
|
98
|
+
error_console.print()
|
|
99
|
+
error_console.print("[dim]Fix the findings above and try again.[/dim]")
|
|
100
|
+
error_console.print(
|
|
101
|
+
"[dim]To bypass (not recommended): [bold]git commit --no-verify[/bold][/dim]"
|
|
102
|
+
)
|
|
103
|
+
error_console.print()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _print_warnings(decision: PolicyDecision) -> None:
|
|
107
|
+
"""Print a WARN result (commit proceeds but warnings shown)."""
|
|
108
|
+
console.print(
|
|
109
|
+
Panel(
|
|
110
|
+
"[yellow]WARNINGS -- commit proceeding[/yellow]",
|
|
111
|
+
border_style="yellow",
|
|
112
|
+
expand=False,
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
console.print()
|
|
116
|
+
|
|
117
|
+
for finding in decision.warning_findings:
|
|
118
|
+
_print_finding(finding, is_blocking=False)
|
|
119
|
+
|
|
120
|
+
console.print()
|
|
121
|
+
_print_pass_line(decision.scan_result)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _print_pass(scan: ScanResult) -> None:
|
|
125
|
+
"""Print a PASS result."""
|
|
126
|
+
_print_pass_line(scan)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _print_pass_line(scan: ScanResult) -> None:
|
|
130
|
+
"""Print the pass summary line."""
|
|
131
|
+
console.print(
|
|
132
|
+
f"[bold green][+][/bold green] Files checked: [bold]{scan.files_scanned}[/bold]"
|
|
133
|
+
)
|
|
134
|
+
console.print("[bold green][+][/bold green] No issues found")
|
|
135
|
+
console.print()
|
|
136
|
+
console.print(
|
|
137
|
+
Panel(
|
|
138
|
+
"[bold green]PASS -- commit allowed[/bold green]",
|
|
139
|
+
border_style="green",
|
|
140
|
+
expand=False,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
console.print()
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _print_finding(finding: Finding, is_blocking: bool) -> None:
|
|
147
|
+
"""Print a single finding."""
|
|
148
|
+
color = SEVERITY_COLORS.get(finding.severity, "white")
|
|
149
|
+
icon = SEVERITY_ICONS.get(finding.severity, "[???] ")
|
|
150
|
+
|
|
151
|
+
# Location string
|
|
152
|
+
location = finding.file
|
|
153
|
+
if finding.line:
|
|
154
|
+
location += f":{finding.line}"
|
|
155
|
+
|
|
156
|
+
# Severity badge
|
|
157
|
+
severity_label = f"[{color}]{finding.severity.value.upper():8}[/{color}]"
|
|
158
|
+
|
|
159
|
+
error_console.print(f" {icon} {severity_label} {location}")
|
|
160
|
+
error_console.print(f" {'':12} [bold]{finding.message}[/bold]")
|
|
161
|
+
|
|
162
|
+
if finding.description:
|
|
163
|
+
error_console.print(f" {'':12} [dim]{finding.description}[/dim]")
|
|
164
|
+
|
|
165
|
+
if finding.evidence:
|
|
166
|
+
error_console.print(f" {'':12} Evidence: [dim]{finding.evidence}[/dim]")
|
|
167
|
+
|
|
168
|
+
if finding.recommendation:
|
|
169
|
+
# Format multi-line recommendations
|
|
170
|
+
lines = finding.recommendation.strip().split("\n")
|
|
171
|
+
for i, line in enumerate(lines):
|
|
172
|
+
prefix = " " if i > 0 else ""
|
|
173
|
+
error_console.print(f" {'':12} [dim]{prefix}{line}[/dim]")
|
|
174
|
+
|
|
175
|
+
error_console.print()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def print_status(
|
|
184
|
+
repo_root: str | None,
|
|
185
|
+
git_available: bool,
|
|
186
|
+
hook_installed: bool,
|
|
187
|
+
hook_path: str | None,
|
|
188
|
+
config_path: str | None,
|
|
189
|
+
git_version: str | None,
|
|
190
|
+
git_binary: str | None = None,
|
|
191
|
+
hooks_dir: str | None = None,
|
|
192
|
+
core_hooks_path: str | None = None,
|
|
193
|
+
yara_available: bool | None = None,
|
|
194
|
+
dependency_ecosystems: tuple[str, ...] | None = None,
|
|
195
|
+
pre_push_installed: bool | None = None,
|
|
196
|
+
pre_push_path: str | None = None,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Print the status of Gitrupt installation."""
|
|
199
|
+
print_header()
|
|
200
|
+
|
|
201
|
+
table = Table(box=box.SIMPLE, show_header=False, padding=(0, 2))
|
|
202
|
+
table.add_column("Item", style="dim", no_wrap=True)
|
|
203
|
+
table.add_column("Status", overflow="fold")
|
|
204
|
+
|
|
205
|
+
def status_row(label: str, ok: bool, value: str = "") -> None:
|
|
206
|
+
icon = "[green][+][/green]" if ok else "[red][-][/red]"
|
|
207
|
+
display = f"{icon} {value}" if value else icon
|
|
208
|
+
table.add_row(label, display)
|
|
209
|
+
|
|
210
|
+
status_row("Git available", git_available, git_version or "not found")
|
|
211
|
+
status_row("Git binary", bool(git_binary), git_binary or "not on PATH")
|
|
212
|
+
status_row("Inside Git repo", repo_root is not None, repo_root or "")
|
|
213
|
+
|
|
214
|
+
if hooks_dir:
|
|
215
|
+
hooks_note = hooks_dir
|
|
216
|
+
if core_hooks_path:
|
|
217
|
+
hooks_note = f"{hooks_dir} [yellow](core.hooksPath override)[/yellow]"
|
|
218
|
+
status_row("Hooks directory", True, hooks_note)
|
|
219
|
+
|
|
220
|
+
status_row(
|
|
221
|
+
"core.hooksPath",
|
|
222
|
+
core_hooks_path is None,
|
|
223
|
+
core_hooks_path if core_hooks_path else "not set (using default)",
|
|
224
|
+
)
|
|
225
|
+
status_row("pre-commit hook", hook_installed, hook_path or "not installed")
|
|
226
|
+
|
|
227
|
+
if pre_push_installed is not None:
|
|
228
|
+
status_row(
|
|
229
|
+
"pre-push hook",
|
|
230
|
+
pre_push_installed,
|
|
231
|
+
pre_push_path if pre_push_installed else "not installed",
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
if yara_available is not None:
|
|
235
|
+
yara_note = "available (full rule engine)" if yara_available else "not installed (fallback mode)"
|
|
236
|
+
status_row("YARA engine", True, yara_note)
|
|
237
|
+
|
|
238
|
+
if dependency_ecosystems is not None:
|
|
239
|
+
if dependency_ecosystems:
|
|
240
|
+
status_row("Dependency ecosystems", True, ", ".join(dependency_ecosystems))
|
|
241
|
+
else:
|
|
242
|
+
status_row("Dependency ecosystems", False, "none registered")
|
|
243
|
+
|
|
244
|
+
status_row("Configuration", config_path is not None, config_path or "using defaults")
|
|
245
|
+
|
|
246
|
+
console.print(table)
|
|
247
|
+
|
|
248
|
+
if core_hooks_path:
|
|
249
|
+
console.print(
|
|
250
|
+
"[yellow]Note:[/yellow] [bold]core.hooksPath[/bold] is set. Git reads hooks from "
|
|
251
|
+
f"[dim]{core_hooks_path}[/dim], not the default [dim].git/hooks[/dim]."
|
|
252
|
+
)
|
|
253
|
+
console.print()
|
|
254
|
+
|
|
255
|
+
if hook_installed and repo_root:
|
|
256
|
+
console.print("[bold green]Protection is active.[/bold green]")
|
|
257
|
+
elif repo_root:
|
|
258
|
+
console.print(
|
|
259
|
+
"[yellow]Hook not installed. Run [bold]gitrupt init[/bold] to enable protection.[/yellow]"
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
console.print("[red]Not inside a Git repository.[/red]")
|
|
263
|
+
console.print()
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def print_init_result(
|
|
267
|
+
repo_root: str,
|
|
268
|
+
hook_paths: list[str],
|
|
269
|
+
was_existing_hook: bool,
|
|
270
|
+
config_exists: bool,
|
|
271
|
+
) -> None:
|
|
272
|
+
"""Print the result of gitrupt init."""
|
|
273
|
+
print_header()
|
|
274
|
+
console.print("Installing security firewall...")
|
|
275
|
+
console.print()
|
|
276
|
+
console.print("[green][+][/green] Repository detected")
|
|
277
|
+
console.print("[green][+][/green] Existing hooks checked")
|
|
278
|
+
if was_existing_hook:
|
|
279
|
+
console.print("[green][+][/green] Existing hook(s) preserved and chained")
|
|
280
|
+
for hp in hook_paths:
|
|
281
|
+
console.print(f"[green][+][/green] Hook installed: [dim]{hp}[/dim]")
|
|
282
|
+
if config_exists:
|
|
283
|
+
console.print("[green][+][/green] Gitrupt configuration found")
|
|
284
|
+
else:
|
|
285
|
+
console.print("[dim] No .gitrupt.yml found -- using defaults[/dim]")
|
|
286
|
+
console.print()
|
|
287
|
+
console.print("[bold green]Protection enabled.[/bold green]")
|
|
288
|
+
console.print()
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def print_uninstall_result(hook_path: str, success: bool) -> None:
|
|
294
|
+
"""Print the result of gitrupt uninstall."""
|
|
295
|
+
print_header()
|
|
296
|
+
if success:
|
|
297
|
+
console.print(f"[green][+][/green] Hook removed: [dim]{hook_path}[/dim]")
|
|
298
|
+
console.print()
|
|
299
|
+
console.print("[yellow]Protection disabled.[/yellow]")
|
|
300
|
+
else:
|
|
301
|
+
console.print("[yellow]No Gitrupt hook found to remove.[/yellow]")
|
|
302
|
+
console.print()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def print_error(message: str) -> None:
|
|
306
|
+
"""Print an error message."""
|
|
307
|
+
error_console.print(f"[red]Error:[/red] {message}")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def print_config(config_dict: dict) -> None:
|
|
311
|
+
"""Print the current configuration."""
|
|
312
|
+
print_header()
|
|
313
|
+
import yaml # type: ignore[import-untyped]
|
|
314
|
+
console.print("[bold]Current configuration:[/bold]")
|
|
315
|
+
console.print()
|
|
316
|
+
console.print(yaml.dump(config_dict, default_flow_style=False, sort_keys=False))
|