zwischen-cli 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.
- zwischen_cli-0.1.0/PKG-INFO +108 -0
- zwischen_cli-0.1.0/README.md +81 -0
- zwischen_cli-0.1.0/pyproject.toml +44 -0
- zwischen_cli-0.1.0/setup.cfg +4 -0
- zwischen_cli-0.1.0/zwischen/__init__.py +22 -0
- zwischen_cli-0.1.0/zwischen/ai.py +180 -0
- zwischen_cli-0.1.0/zwischen/cli.py +39 -0
- zwischen_cli-0.1.0/zwischen/config.py +103 -0
- zwischen_cli-0.1.0/zwischen/detector.py +191 -0
- zwischen_cli-0.1.0/zwischen/doctor.py +63 -0
- zwischen_cli-0.1.0/zwischen/init.py +72 -0
- zwischen_cli-0.1.0/zwischen/installer.py +122 -0
- zwischen_cli-0.1.0/zwischen/scanner.py +256 -0
- zwischen_cli-0.1.0/zwischen_cli.egg-info/PKG-INFO +108 -0
- zwischen_cli-0.1.0/zwischen_cli.egg-info/SOURCES.txt +17 -0
- zwischen_cli-0.1.0/zwischen_cli.egg-info/dependency_links.txt +1 -0
- zwischen_cli-0.1.0/zwischen_cli.egg-info/entry_points.txt +2 -0
- zwischen_cli-0.1.0/zwischen_cli.egg-info/requires.txt +3 -0
- zwischen_cli-0.1.0/zwischen_cli.egg-info/top_level.txt +2 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zwischen-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-augmented security scanning for vibe coders. Zero-config secrets detection and vulnerability scanning.
|
|
5
|
+
Author-email: Conner Jordan <connercharlesjordan@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cjordan223/zwischen
|
|
8
|
+
Project-URL: Repository, https://github.com/cjordan223/zwischen.git
|
|
9
|
+
Project-URL: Issues, https://github.com/cjordan223/Zwischen/issues
|
|
10
|
+
Keywords: security,scanner,secrets,gitleaks,semgrep,ai,vulnerability,sast
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: click>=8.0.0
|
|
25
|
+
Requires-Dist: pyyaml>=6.0
|
|
26
|
+
Requires-Dist: requests>=2.28.0
|
|
27
|
+
|
|
28
|
+
# Zwischen Python Package
|
|
29
|
+
|
|
30
|
+
Python wrapper for Zwischen, an AI-augmented security scanning CLI. This package exposes a Python implementation of the core workflow for Python users.
|
|
31
|
+
|
|
32
|
+
The Ruby gem in the repository root is currently the canonical implementation. This wrapper has a smaller command surface and may not match every Ruby feature.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install zwischen-cli
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The PyPI distribution is named `zwischen-cli` (the bare `zwischen` name is taken by an unrelated project), but the installed command is still `zwischen`.
|
|
41
|
+
|
|
42
|
+
For local development:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
cd packages/pip
|
|
46
|
+
python -m pip install -e .
|
|
47
|
+
zwischen --help
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
zwischen init
|
|
54
|
+
zwischen scan
|
|
55
|
+
zwischen scan --ai ollama
|
|
56
|
+
zwischen scan --ai openai --api-key "$OPENAI_API_KEY"
|
|
57
|
+
zwischen scan --format json
|
|
58
|
+
zwischen scan --pre-push
|
|
59
|
+
zwischen doctor
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Supported scan flags:
|
|
63
|
+
|
|
64
|
+
- `--ai`: `ollama`, `openai`, or `anthropic`
|
|
65
|
+
- `--api-key`: provider API key
|
|
66
|
+
- `--format`: `terminal` or `json`
|
|
67
|
+
- `--pre-push`: compact hook mode
|
|
68
|
+
|
|
69
|
+
Not currently supported in this wrapper:
|
|
70
|
+
|
|
71
|
+
- `zwischen uninstall`
|
|
72
|
+
- `zwischen scan --only ...`
|
|
73
|
+
- Ruby's changed-file filtering for `--pre-push`
|
|
74
|
+
|
|
75
|
+
## Behavior
|
|
76
|
+
|
|
77
|
+
`zwischen init` tries to install Gitleaks into `~/.zwischen/bin`, creates `.zwischen.yml`, checks whether Semgrep is available, and installs or appends a Git `pre-push` hook when run inside a Git repository.
|
|
78
|
+
|
|
79
|
+
Semgrep is optional:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install semgrep
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Configuration
|
|
86
|
+
|
|
87
|
+
The Python wrapper creates this shape:
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
ai:
|
|
91
|
+
enabled: true
|
|
92
|
+
pre_push_enabled: false
|
|
93
|
+
provider: ollama
|
|
94
|
+
model: llama3
|
|
95
|
+
|
|
96
|
+
blocking:
|
|
97
|
+
severity: high
|
|
98
|
+
|
|
99
|
+
scanners:
|
|
100
|
+
gitleaks: true
|
|
101
|
+
semgrep: true
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Blocking severities are `high`, `critical`, or `none`.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Zwischen Python Package
|
|
2
|
+
|
|
3
|
+
Python wrapper for Zwischen, an AI-augmented security scanning CLI. This package exposes a Python implementation of the core workflow for Python users.
|
|
4
|
+
|
|
5
|
+
The Ruby gem in the repository root is currently the canonical implementation. This wrapper has a smaller command surface and may not match every Ruby feature.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install zwischen-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The PyPI distribution is named `zwischen-cli` (the bare `zwischen` name is taken by an unrelated project), but the installed command is still `zwischen`.
|
|
14
|
+
|
|
15
|
+
For local development:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd packages/pip
|
|
19
|
+
python -m pip install -e .
|
|
20
|
+
zwischen --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
zwischen init
|
|
27
|
+
zwischen scan
|
|
28
|
+
zwischen scan --ai ollama
|
|
29
|
+
zwischen scan --ai openai --api-key "$OPENAI_API_KEY"
|
|
30
|
+
zwischen scan --format json
|
|
31
|
+
zwischen scan --pre-push
|
|
32
|
+
zwischen doctor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Supported scan flags:
|
|
36
|
+
|
|
37
|
+
- `--ai`: `ollama`, `openai`, or `anthropic`
|
|
38
|
+
- `--api-key`: provider API key
|
|
39
|
+
- `--format`: `terminal` or `json`
|
|
40
|
+
- `--pre-push`: compact hook mode
|
|
41
|
+
|
|
42
|
+
Not currently supported in this wrapper:
|
|
43
|
+
|
|
44
|
+
- `zwischen uninstall`
|
|
45
|
+
- `zwischen scan --only ...`
|
|
46
|
+
- Ruby's changed-file filtering for `--pre-push`
|
|
47
|
+
|
|
48
|
+
## Behavior
|
|
49
|
+
|
|
50
|
+
`zwischen init` tries to install Gitleaks into `~/.zwischen/bin`, creates `.zwischen.yml`, checks whether Semgrep is available, and installs or appends a Git `pre-push` hook when run inside a Git repository.
|
|
51
|
+
|
|
52
|
+
Semgrep is optional:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install semgrep
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
The Python wrapper creates this shape:
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
ai:
|
|
64
|
+
enabled: true
|
|
65
|
+
pre_push_enabled: false
|
|
66
|
+
provider: ollama
|
|
67
|
+
model: llama3
|
|
68
|
+
|
|
69
|
+
blocking:
|
|
70
|
+
severity: high
|
|
71
|
+
|
|
72
|
+
scanners:
|
|
73
|
+
gitleaks: true
|
|
74
|
+
semgrep: true
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Blocking severities are `high`, `critical`, or `none`.
|
|
78
|
+
|
|
79
|
+
## License
|
|
80
|
+
|
|
81
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zwischen-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "AI-augmented security scanning for vibe coders. Zero-config secrets detection and vulnerability scanning."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Conner Jordan", email = "connercharlesjordan@gmail.com"}
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Security",
|
|
25
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
26
|
+
]
|
|
27
|
+
keywords = ["security", "scanner", "secrets", "gitleaks", "semgrep", "ai", "vulnerability", "sast"]
|
|
28
|
+
requires-python = ">=3.9"
|
|
29
|
+
dependencies = [
|
|
30
|
+
"click>=8.0.0",
|
|
31
|
+
"pyyaml>=6.0",
|
|
32
|
+
"requests>=2.28.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
zwischen = "zwischen.cli:main"
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/cjordan223/zwischen"
|
|
40
|
+
Repository = "https://github.com/cjordan223/zwischen.git"
|
|
41
|
+
Issues = "https://github.com/cjordan223/Zwischen/issues"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["."]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Zwischen - AI-augmented security scanning for vibe coders."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from .scanner import scan, run_gitleaks, run_semgrep
|
|
6
|
+
from .installer import install_gitleaks, get_gitleaks_path, is_gitleaks_installed
|
|
7
|
+
from .config import load_config, create_config
|
|
8
|
+
from .ai import analyze_with_ai
|
|
9
|
+
from .detector import detect_project
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"scan",
|
|
13
|
+
"run_gitleaks",
|
|
14
|
+
"run_semgrep",
|
|
15
|
+
"install_gitleaks",
|
|
16
|
+
"get_gitleaks_path",
|
|
17
|
+
"is_gitleaks_installed",
|
|
18
|
+
"load_config",
|
|
19
|
+
"create_config",
|
|
20
|
+
"analyze_with_ai",
|
|
21
|
+
"detect_project",
|
|
22
|
+
]
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""AI provider clients for Zwischen."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _build_prompt(findings: list[dict]) -> str:
|
|
12
|
+
"""Build AI analysis prompt."""
|
|
13
|
+
def format_finding(i: int, f: dict) -> str:
|
|
14
|
+
code_line = f" Code: {f['code_snippet']}" if f.get('code_snippet') else ""
|
|
15
|
+
return (
|
|
16
|
+
f"{i + 1}. [{(f.get('severity') or 'medium').upper()}] {f['file']}:{f['line']}\n"
|
|
17
|
+
f" Rule: {f.get('rule_id', 'unknown')}\n"
|
|
18
|
+
f" Message: {f.get('message', '')}\n"
|
|
19
|
+
f"{code_line}"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
findings_text = "\n\n".join(format_finding(i, f) for i, f in enumerate(findings))
|
|
23
|
+
|
|
24
|
+
return f"""You are a senior security engineer reviewing security scan findings. Analyze the following findings and provide:
|
|
25
|
+
|
|
26
|
+
1. Prioritization: Which findings are most critical and should be addressed first?
|
|
27
|
+
2. False positives: Are any of these false positives that can be safely ignored?
|
|
28
|
+
3. Fix suggestions: For each real finding, provide a clear, actionable fix suggestion.
|
|
29
|
+
|
|
30
|
+
Findings:
|
|
31
|
+
{findings_text}
|
|
32
|
+
|
|
33
|
+
Please respond in the following JSON format for each finding (by index number):
|
|
34
|
+
{{
|
|
35
|
+
"1": {{
|
|
36
|
+
"priority": "high|medium|low",
|
|
37
|
+
"is_false_positive": false,
|
|
38
|
+
"fix_suggestion": "Clear explanation of how to fix this issue",
|
|
39
|
+
"risk_explanation": "Why this is a security risk"
|
|
40
|
+
}}
|
|
41
|
+
}}
|
|
42
|
+
|
|
43
|
+
If a finding is a false positive, set is_false_positive to true and explain why."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _call_ollama(prompt: str, config: dict) -> str:
|
|
47
|
+
"""Call Ollama API."""
|
|
48
|
+
base_url = config.get("url", "http://localhost:11434")
|
|
49
|
+
model = config.get("model", "llama3")
|
|
50
|
+
|
|
51
|
+
response = requests.post(
|
|
52
|
+
f"{base_url}/api/chat",
|
|
53
|
+
json={
|
|
54
|
+
"model": model,
|
|
55
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
56
|
+
"stream": False,
|
|
57
|
+
},
|
|
58
|
+
timeout=120,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if response.status_code != 200:
|
|
62
|
+
raise Exception(f"Ollama error: {response.text}")
|
|
63
|
+
|
|
64
|
+
data = response.json()
|
|
65
|
+
if "error" in data:
|
|
66
|
+
raise Exception(f"Ollama error: {data['error']}")
|
|
67
|
+
|
|
68
|
+
return data.get("message", {}).get("content", "")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _call_openai(prompt: str, config: dict) -> str:
|
|
72
|
+
"""Call OpenAI API."""
|
|
73
|
+
api_key = config.get("api_key") or os.environ.get("OPENAI_API_KEY")
|
|
74
|
+
if not api_key:
|
|
75
|
+
raise Exception("OpenAI API key not found. Set OPENAI_API_KEY or provide --api-key")
|
|
76
|
+
|
|
77
|
+
model = config.get("model", "gpt-4o-mini")
|
|
78
|
+
|
|
79
|
+
response = requests.post(
|
|
80
|
+
"https://api.openai.com/v1/chat/completions",
|
|
81
|
+
headers={
|
|
82
|
+
"Authorization": f"Bearer {api_key}",
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
},
|
|
85
|
+
json={
|
|
86
|
+
"model": model,
|
|
87
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
88
|
+
},
|
|
89
|
+
timeout=120,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if response.status_code != 200:
|
|
93
|
+
raise Exception(f"OpenAI error: {response.text}")
|
|
94
|
+
|
|
95
|
+
data = response.json()
|
|
96
|
+
if "error" in data:
|
|
97
|
+
raise Exception(f"OpenAI error: {data['error']['message']}")
|
|
98
|
+
|
|
99
|
+
return data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _call_anthropic(prompt: str, config: dict) -> str:
|
|
103
|
+
"""Call Anthropic API."""
|
|
104
|
+
api_key = config.get("api_key") or os.environ.get("ANTHROPIC_API_KEY")
|
|
105
|
+
if not api_key:
|
|
106
|
+
raise Exception("Anthropic API key not found. Set ANTHROPIC_API_KEY or provide --api-key")
|
|
107
|
+
|
|
108
|
+
model = config.get("model", "claude-3-haiku-20240307")
|
|
109
|
+
|
|
110
|
+
response = requests.post(
|
|
111
|
+
"https://api.anthropic.com/v1/messages",
|
|
112
|
+
headers={
|
|
113
|
+
"x-api-key": api_key,
|
|
114
|
+
"anthropic-version": "2023-06-01",
|
|
115
|
+
"Content-Type": "application/json",
|
|
116
|
+
},
|
|
117
|
+
json={
|
|
118
|
+
"model": model,
|
|
119
|
+
"max_tokens": 4096,
|
|
120
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
121
|
+
},
|
|
122
|
+
timeout=120,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if response.status_code != 200:
|
|
126
|
+
raise Exception(f"Anthropic error: {response.text}")
|
|
127
|
+
|
|
128
|
+
data = response.json()
|
|
129
|
+
if "error" in data:
|
|
130
|
+
raise Exception(f"Anthropic error: {data['error']['message']}")
|
|
131
|
+
|
|
132
|
+
return data.get("content", [{}])[0].get("text", "")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def analyze_with_ai(
|
|
136
|
+
findings: list[dict],
|
|
137
|
+
provider: str = "ollama",
|
|
138
|
+
api_key: str | None = None,
|
|
139
|
+
**config,
|
|
140
|
+
) -> list[dict]:
|
|
141
|
+
"""Analyze findings with AI."""
|
|
142
|
+
if not findings:
|
|
143
|
+
return findings
|
|
144
|
+
|
|
145
|
+
prompt = _build_prompt(findings)
|
|
146
|
+
config["api_key"] = api_key
|
|
147
|
+
|
|
148
|
+
provider = provider.lower()
|
|
149
|
+
if provider == "ollama":
|
|
150
|
+
response = _call_ollama(prompt, config)
|
|
151
|
+
elif provider == "openai":
|
|
152
|
+
response = _call_openai(prompt, config)
|
|
153
|
+
elif provider in ("anthropic", "claude"):
|
|
154
|
+
response = _call_anthropic(prompt, config)
|
|
155
|
+
else:
|
|
156
|
+
raise Exception(f"Unknown AI provider: {provider}")
|
|
157
|
+
|
|
158
|
+
# Parse AI response
|
|
159
|
+
try:
|
|
160
|
+
json_match = re.search(r"\{[\s\S]*\}", response)
|
|
161
|
+
if not json_match:
|
|
162
|
+
return findings
|
|
163
|
+
|
|
164
|
+
analysis = json.loads(json_match.group())
|
|
165
|
+
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
**f,
|
|
169
|
+
"ai_priority": analysis.get(str(i + 1), {}).get("priority"),
|
|
170
|
+
"ai_false_positive": analysis.get(str(i + 1), {}).get("is_false_positive", False),
|
|
171
|
+
"ai_fix_suggestion": analysis.get(str(i + 1), {}).get("fix_suggestion"),
|
|
172
|
+
"ai_risk_explanation": analysis.get(str(i + 1), {}).get("risk_explanation"),
|
|
173
|
+
}
|
|
174
|
+
for i, f in enumerate(findings)
|
|
175
|
+
]
|
|
176
|
+
|
|
177
|
+
except (json.JSONDecodeError, Exception) as e:
|
|
178
|
+
if os.environ.get("DEBUG"):
|
|
179
|
+
print(f"Failed to parse AI response: {e}")
|
|
180
|
+
return findings
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Command-line interface for Zwischen."""
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from .scanner import scan as do_scan
|
|
5
|
+
from .init import init as do_init
|
|
6
|
+
from .doctor import doctor as do_doctor
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
@click.version_option()
|
|
11
|
+
def main():
|
|
12
|
+
"""AI-augmented security scanning for vibe coders."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@main.command()
|
|
17
|
+
def init():
|
|
18
|
+
"""Initialize Zwischen in your project."""
|
|
19
|
+
do_init()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@main.command()
|
|
23
|
+
@click.option("--ai", help="AI provider (ollama, openai, anthropic)")
|
|
24
|
+
@click.option("--api-key", help="API key for AI provider")
|
|
25
|
+
@click.option("--format", "output_format", default="terminal", help="Output format (terminal, json)")
|
|
26
|
+
@click.option("--pre-push", is_flag=True, help="Pre-push mode (compact output)")
|
|
27
|
+
def scan(ai, api_key, output_format, pre_push):
|
|
28
|
+
"""Run security scan."""
|
|
29
|
+
do_scan(ai=ai, api_key=api_key, output_format=output_format, pre_push=pre_push)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@main.command()
|
|
33
|
+
def doctor():
|
|
34
|
+
"""Check if required tools are installed."""
|
|
35
|
+
do_doctor()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
main()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Configuration handling for Zwischen."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
import yaml
|
|
6
|
+
|
|
7
|
+
DEFAULT_CONFIG = {
|
|
8
|
+
"ai": {
|
|
9
|
+
"enabled": True,
|
|
10
|
+
"pre_push_enabled": False,
|
|
11
|
+
"provider": "ollama",
|
|
12
|
+
"model": "llama3",
|
|
13
|
+
},
|
|
14
|
+
"blocking": {
|
|
15
|
+
"severity": "high",
|
|
16
|
+
},
|
|
17
|
+
"scanners": {
|
|
18
|
+
"gitleaks": {"enabled": True},
|
|
19
|
+
"semgrep": {"enabled": True, "config": "p/security-audit"},
|
|
20
|
+
},
|
|
21
|
+
"ignore": [
|
|
22
|
+
"**/node_modules/**",
|
|
23
|
+
"**/vendor/**",
|
|
24
|
+
"**/.git/**",
|
|
25
|
+
"**/dist/**",
|
|
26
|
+
"**/build/**",
|
|
27
|
+
"**/test/fixtures/**",
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
EXAMPLE_CONFIG = """# Zwischen Configuration
|
|
32
|
+
|
|
33
|
+
# AI Provider Configuration
|
|
34
|
+
ai:
|
|
35
|
+
enabled: true
|
|
36
|
+
pre_push_enabled: false # Disable AI in pre-push hooks (performance)
|
|
37
|
+
provider: ollama # Options: ollama, openai, anthropic
|
|
38
|
+
model: llama3 # Model name for your provider
|
|
39
|
+
# url: http://localhost:11434 # For Ollama (default)
|
|
40
|
+
# api_key: null # For OpenAI/Anthropic (or use env vars)
|
|
41
|
+
|
|
42
|
+
# What blocks a push
|
|
43
|
+
blocking:
|
|
44
|
+
severity: high # block on high or critical (default)
|
|
45
|
+
# severity: critical # only block on critical
|
|
46
|
+
# severity: none # never block, just warn
|
|
47
|
+
|
|
48
|
+
# Scanner Configuration
|
|
49
|
+
scanners:
|
|
50
|
+
gitleaks: true # Auto-installed if missing
|
|
51
|
+
semgrep: true # Optional, install with: pip install semgrep
|
|
52
|
+
|
|
53
|
+
# Ignored Paths (glob patterns)
|
|
54
|
+
ignore:
|
|
55
|
+
- "**/node_modules/**"
|
|
56
|
+
- "**/vendor/**"
|
|
57
|
+
- "**/.git/**"
|
|
58
|
+
- "**/dist/**"
|
|
59
|
+
- "**/build/**"
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def deep_merge(base: dict, override: dict) -> dict:
|
|
64
|
+
"""Deep merge two dictionaries."""
|
|
65
|
+
result = base.copy()
|
|
66
|
+
for key, value in override.items():
|
|
67
|
+
if (
|
|
68
|
+
key in result
|
|
69
|
+
and isinstance(result[key], dict)
|
|
70
|
+
and isinstance(value, dict)
|
|
71
|
+
):
|
|
72
|
+
result[key] = deep_merge(result[key], value)
|
|
73
|
+
else:
|
|
74
|
+
result[key] = value
|
|
75
|
+
return result
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_config(project_root: str | Path = ".") -> dict:
|
|
79
|
+
"""Load configuration from .zwischen.yml."""
|
|
80
|
+
config_path = Path(project_root) / ".zwischen.yml"
|
|
81
|
+
|
|
82
|
+
if not config_path.exists():
|
|
83
|
+
return DEFAULT_CONFIG.copy()
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(config_path) as f:
|
|
87
|
+
user_config = yaml.safe_load(f) or {}
|
|
88
|
+
return deep_merge(DEFAULT_CONFIG, user_config)
|
|
89
|
+
except Exception as e:
|
|
90
|
+
print(f"Warning: Could not parse .zwischen.yml: {e}")
|
|
91
|
+
return DEFAULT_CONFIG.copy()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def create_config(project_root: str | Path = ".") -> bool:
|
|
95
|
+
"""Create configuration file."""
|
|
96
|
+
config_path = Path(project_root) / ".zwischen.yml"
|
|
97
|
+
|
|
98
|
+
if config_path.exists():
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
with open(config_path, "w") as f:
|
|
102
|
+
f.write(EXAMPLE_CONFIG)
|
|
103
|
+
return True
|