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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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