rafter-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,127 @@
1
+ Metadata-Version: 2.3
2
+ Name: rafter-cli
3
+ Version: 0.1.0
4
+ Summary: Rafter CLI
5
+ License: MIT
6
+ Author: Rafter Team
7
+ Author-email: hello@rafter.so
8
+ Requires-Python: >=3.8,<4.0
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
18
+ Requires-Dist: requests (>=2.31.0,<3.0.0)
19
+ Requires-Dist: rich (>=13.7.1,<14.0.0)
20
+ Requires-Dist: typer (>=0.12.3,<0.13.0)
21
+ Description-Content-Type: text/markdown
22
+
23
+ # rafter-cli
24
+
25
+ A Python CLI for Rafter Security that supports pip package management.
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ # Using pip
31
+ pip install rafter-cli
32
+
33
+ # Using pipx (recommended for CLI tools)
34
+ pipx install rafter-cli
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```bash
40
+ # Set your API key
41
+ export RAFTER_API_KEY="your-api-key-here"
42
+
43
+ # Run a security scan
44
+ rafter run
45
+
46
+ # Get scan results
47
+ rafter get <scan-id>
48
+
49
+ # Check API usage
50
+ rafter usage
51
+ ```
52
+
53
+ ## Commands
54
+
55
+ ### `rafter run [options]`
56
+
57
+ Trigger a new security scan for your repository.
58
+
59
+ **Options:**
60
+ - `-r, --repo <repo>` - Repository in format `org/repo` (default: auto-detected)
61
+ - `-b, --branch <branch>` - Branch name (default: auto-detected)
62
+ - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
63
+ - `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
64
+ - `--skip-interactive` - Don't wait for scan completion
65
+ - `--quiet` - Suppress status messages
66
+
67
+ **Examples:**
68
+ ```bash
69
+ # Basic scan with auto-detection
70
+ rafter run
71
+
72
+ # Scan specific repo/branch
73
+ rafter run --repo myorg/myrepo --branch feature-branch
74
+
75
+ # Non-interactive scan
76
+ rafter run --skip-interactive
77
+ ```
78
+
79
+ ### `rafter get <scan-id> [options]`
80
+
81
+ Retrieve results from a completed scan.
82
+
83
+ **Options:**
84
+ - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
85
+ - `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
86
+ - `--interactive` - Poll until scan completes
87
+ - `--quiet` - Suppress status messages
88
+
89
+ **Examples:**
90
+ ```bash
91
+ # Get scan results
92
+ rafter get <scan-id>
93
+
94
+ # Wait for scan completion
95
+ rafter get <scan-id> --interactive
96
+ ```
97
+
98
+ ### `rafter usage [options]`
99
+
100
+ Check your API quota and usage.
101
+
102
+ **Options:**
103
+ - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
104
+
105
+ **Example:**
106
+ ```bash
107
+ rafter usage
108
+ ```
109
+
110
+ ## Configuration
111
+
112
+ ### Environment Variables
113
+
114
+ - `RAFTER_API_KEY` - Your Rafter API key (alternative to `--api-key` flag)
115
+
116
+ ### Git Auto-Detection
117
+
118
+ The CLI automatically detects your repository and branch from the current Git repository:
119
+
120
+ 1. **Repository**: Extracted from Git remote URL
121
+ 2. **Branch**: Current branch name, or `main`
122
+
123
+ **Note**: The CLI only scans remote repositories, not your current local branch.
124
+
125
+ ## Documentation
126
+
127
+ For comprehensive documentation, API reference, and examples, see [https://docs.rafter.so](https://docs.rafter.so).
@@ -0,0 +1,105 @@
1
+ # rafter-cli
2
+
3
+ A Python CLI for Rafter Security that supports pip package management.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Using pip
9
+ pip install rafter-cli
10
+
11
+ # Using pipx (recommended for CLI tools)
12
+ pipx install rafter-cli
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # Set your API key
19
+ export RAFTER_API_KEY="your-api-key-here"
20
+
21
+ # Run a security scan
22
+ rafter run
23
+
24
+ # Get scan results
25
+ rafter get <scan-id>
26
+
27
+ # Check API usage
28
+ rafter usage
29
+ ```
30
+
31
+ ## Commands
32
+
33
+ ### `rafter run [options]`
34
+
35
+ Trigger a new security scan for your repository.
36
+
37
+ **Options:**
38
+ - `-r, --repo <repo>` - Repository in format `org/repo` (default: auto-detected)
39
+ - `-b, --branch <branch>` - Branch name (default: auto-detected)
40
+ - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
41
+ - `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
42
+ - `--skip-interactive` - Don't wait for scan completion
43
+ - `--quiet` - Suppress status messages
44
+
45
+ **Examples:**
46
+ ```bash
47
+ # Basic scan with auto-detection
48
+ rafter run
49
+
50
+ # Scan specific repo/branch
51
+ rafter run --repo myorg/myrepo --branch feature-branch
52
+
53
+ # Non-interactive scan
54
+ rafter run --skip-interactive
55
+ ```
56
+
57
+ ### `rafter get <scan-id> [options]`
58
+
59
+ Retrieve results from a completed scan.
60
+
61
+ **Options:**
62
+ - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
63
+ - `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
64
+ - `--interactive` - Poll until scan completes
65
+ - `--quiet` - Suppress status messages
66
+
67
+ **Examples:**
68
+ ```bash
69
+ # Get scan results
70
+ rafter get <scan-id>
71
+
72
+ # Wait for scan completion
73
+ rafter get <scan-id> --interactive
74
+ ```
75
+
76
+ ### `rafter usage [options]`
77
+
78
+ Check your API quota and usage.
79
+
80
+ **Options:**
81
+ - `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
82
+
83
+ **Example:**
84
+ ```bash
85
+ rafter usage
86
+ ```
87
+
88
+ ## Configuration
89
+
90
+ ### Environment Variables
91
+
92
+ - `RAFTER_API_KEY` - Your Rafter API key (alternative to `--api-key` flag)
93
+
94
+ ### Git Auto-Detection
95
+
96
+ The CLI automatically detects your repository and branch from the current Git repository:
97
+
98
+ 1. **Repository**: Extracted from Git remote URL
99
+ 2. **Branch**: Current branch name, or `main`
100
+
101
+ **Note**: The CLI only scans remote repositories, not your current local branch.
102
+
103
+ ## Documentation
104
+
105
+ For comprehensive documentation, API reference, and examples, see [https://docs.rafter.so](https://docs.rafter.so).
@@ -0,0 +1,25 @@
1
+ [tool.poetry]
2
+ name = "rafter-cli"
3
+ version = "0.1.0"
4
+ description = "Rafter CLI"
5
+ authors = ["Rafter Team <hello@rafter.so>"]
6
+ license = "MIT"
7
+ readme = "README.md"
8
+
9
+ [tool.poetry.dependencies]
10
+ python = ">=3.8,<4.0"
11
+ typer = "^0.12.3"
12
+ rich = "^13.7.1"
13
+ python-dotenv = "^1.0.1"
14
+ requests = "^2.31.0"
15
+
16
+ [tool.poetry.scripts]
17
+ rafter = "rafter_cli.__main__:app"
18
+
19
+ [tool.poetry.group.dev.dependencies]
20
+ pytest = "^8.2.0"
21
+ pytest-mock = "^3.14.0"
22
+
23
+ [build-system]
24
+ requires = ["poetry-core>=1.0.0"]
25
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,219 @@
1
+ import typer
2
+ import requests
3
+ import time
4
+ import os
5
+ import json
6
+ import pathlib
7
+ import subprocess
8
+ import re
9
+ import sys
10
+ from dotenv import load_dotenv
11
+ from rich import print, progress
12
+
13
+ app = typer.Typer(
14
+ name="rafter",
15
+ help="Rafter CLI",
16
+ add_completion=False,
17
+ no_args_is_help=True,
18
+ )
19
+
20
+ API_BASE = "https://rafter.so/api"
21
+
22
+ # Exit codes
23
+ EXIT_SUCCESS = 0
24
+ EXIT_GENERAL_ERROR = 1
25
+ EXIT_SCAN_NOT_FOUND = 2
26
+ EXIT_QUOTA_EXHAUSTED = 3
27
+
28
+ class GitInfo:
29
+ def __init__(self):
30
+ self.inside_repo = self._run(["git", "rev-parse", "--is-inside-work-tree"]) == "true"
31
+ if not self.inside_repo:
32
+ raise RuntimeError("Not inside a Git repository")
33
+ self.root = pathlib.Path(self._run(["git", "rev-parse", "--show-toplevel"]))
34
+ self.branch = self._detect_branch()
35
+ self.repo_slug = self._parse_remote(self._run(["git", "remote", "get-url", "origin"]))
36
+
37
+ def _run(self, cmd):
38
+ return subprocess.check_output(cmd, text=True).strip()
39
+
40
+ def _detect_branch(self):
41
+ try:
42
+ return self._run(["git", "symbolic-ref", "--quiet", "--short", "HEAD"])
43
+ except subprocess.CalledProcessError:
44
+ try:
45
+ return self._run(["git", "rev-parse", "--short", "HEAD"])
46
+ except subprocess.CalledProcessError:
47
+ return "main"
48
+
49
+ def _parse_remote(self, url: str) -> str:
50
+ url = re.sub(r"^(https?://|git@)", "", url)
51
+ url = url.replace(":", "/")
52
+ url = url[:-4] if url.endswith(".git") else url
53
+ return "/".join(url.split("/")[-2:])
54
+
55
+ def resolve_key(cli_opt):
56
+ if cli_opt:
57
+ return cli_opt
58
+ load_dotenv()
59
+ env_key = os.getenv("RAFTER_API_KEY")
60
+ if env_key:
61
+ return env_key
62
+ print("No API key provided. Use --api-key or set RAFTER_API_KEY", file=sys.stderr)
63
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
64
+
65
+ def resolve_repo_branch(repo_opt, branch_opt, quiet):
66
+ if repo_opt and branch_opt:
67
+ return repo_opt, branch_opt
68
+ repo_env = os.getenv("GITHUB_REPOSITORY") or os.getenv("CI_REPOSITORY")
69
+ branch_env = os.getenv("GITHUB_REF_NAME") or os.getenv("CI_COMMIT_BRANCH") or os.getenv("CI_BRANCH")
70
+ repo = repo_opt or repo_env
71
+ branch = branch_opt or branch_env
72
+ try:
73
+ if not repo or not branch:
74
+ git = GitInfo()
75
+ if not repo:
76
+ repo = git.repo_slug
77
+ if not branch:
78
+ branch = git.branch
79
+ if not repo_opt or not branch_opt:
80
+ if not quiet:
81
+ print(f"Repo auto-detected: {repo} @ {branch} (note: scanning remote)", file=sys.stderr)
82
+ return repo, branch
83
+ except Exception:
84
+ print("Could not auto-detect Git repository. Please pass --repo and --branch explicitly.", file=sys.stderr)
85
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
86
+
87
+ def write_payload(data, fmt="json", quiet=False):
88
+ """Write payload to stdout following UNIX principles"""
89
+ if fmt == "md":
90
+ payload = data.get("markdown", "")
91
+ else:
92
+ payload = json.dumps(data, indent=2 if not quiet else None)
93
+
94
+ # Stream to stdout for pipelines
95
+ sys.stdout.write(payload)
96
+ return EXIT_SUCCESS
97
+
98
+ def handle_scan_status_interactive(scan_id, headers, fmt, quiet):
99
+ # First poll
100
+ poll = requests.get(f"{API_BASE}/static/scan", headers=headers, params={"scan_id": scan_id, "format": fmt})
101
+
102
+ if poll.status_code == 404:
103
+ print(f"Scan '{scan_id}' not found", file=sys.stderr)
104
+ raise typer.Exit(code=EXIT_SCAN_NOT_FOUND)
105
+ elif poll.status_code != 200:
106
+ print(f"Error: {poll.text}", file=sys.stderr)
107
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
108
+
109
+ data = poll.json()
110
+ status = data.get("status")
111
+
112
+ if status in ("queued", "pending", "processing"):
113
+ if not quiet:
114
+ print("Waiting for scan to complete... (this could take several minutes)", file=sys.stderr)
115
+ while status in ("queued", "pending", "processing"):
116
+ time.sleep(10)
117
+ poll = requests.get(f"{API_BASE}/static/scan", headers=headers, params={"scan_id": scan_id, "format": fmt})
118
+ data = poll.json()
119
+ status = data.get("status")
120
+ if status == "completed":
121
+ if not quiet:
122
+ print("Scan completed!", file=sys.stderr)
123
+ return write_payload(data, fmt, quiet)
124
+ elif status == "failed":
125
+ print("Scan failed.", file=sys.stderr)
126
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
127
+ if not quiet:
128
+ print(f"Scan status: {status}", file=sys.stderr)
129
+ elif status == "completed":
130
+ if not quiet:
131
+ print("Scan completed!", file=sys.stderr)
132
+ return write_payload(data, fmt, quiet)
133
+ elif status == "failed":
134
+ print("Scan failed.", file=sys.stderr)
135
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
136
+ else:
137
+ if not quiet:
138
+ print(f"Scan status: {status}", file=sys.stderr)
139
+
140
+ return write_payload(data, fmt, quiet)
141
+
142
+ @app.command()
143
+ def run(
144
+ repo: str = typer.Option(None, "--repo", "-r", help="org/repo (default: current)"),
145
+ branch: str = typer.Option(None, "--branch", "-b", help="branch (default: current else main)"),
146
+ api_key: str = typer.Option(None, "--api-key", "-k", envvar="RAFTER_API_KEY", help="API key or RAFTER_API_KEY env var"),
147
+ fmt: str = typer.Option("json", "--format", "-f", help="json | md"),
148
+ skip_interactive: bool = typer.Option(False, "--skip-interactive", help="do not wait for scan to complete"),
149
+ quiet: bool = typer.Option(False, "--quiet", help="suppress status messages"),
150
+ ):
151
+ key = resolve_key(api_key)
152
+ repo, branch = resolve_repo_branch(repo, branch, quiet)
153
+ headers = {"x-api-key": key, "Content-Type": "application/json"}
154
+
155
+ resp = requests.post(f"{API_BASE}/static/scan", headers=headers, json={"repository_name": repo, "branch_name": branch})
156
+
157
+ if resp.status_code == 429:
158
+ print("Quota exhausted", file=sys.stderr)
159
+ raise typer.Exit(code=EXIT_QUOTA_EXHAUSTED)
160
+ elif resp.status_code != 200:
161
+ print(f"Error: {resp.text}", file=sys.stderr)
162
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
163
+
164
+ scan_id = resp.json()["scan_id"]
165
+ if not quiet:
166
+ print(f"Scan ID: {scan_id}", file=sys.stderr)
167
+
168
+ if skip_interactive:
169
+ return
170
+
171
+ handle_scan_status_interactive(scan_id, headers, fmt, quiet)
172
+
173
+ @app.command()
174
+ def get(
175
+ scan_id: str = typer.Argument(...),
176
+ api_key: str = typer.Option(None, "--api-key", "-k", envvar="RAFTER_API_KEY", help="API key or RAFTER_API_KEY env var"),
177
+ fmt: str = typer.Option("json", "--format", "-f", help="json | md"),
178
+ interactive: bool = typer.Option(False, "--interactive", help="poll until done"),
179
+ quiet: bool = typer.Option(False, "--quiet", help="suppress status messages"),
180
+ ):
181
+ key = resolve_key(api_key)
182
+ headers = {"x-api-key": key}
183
+
184
+ if not interactive:
185
+ resp = requests.get(f"{API_BASE}/static/scan", headers=headers, params={"scan_id": scan_id, "format": fmt})
186
+
187
+ if resp.status_code == 404:
188
+ print(f"Scan '{scan_id}' not found", file=sys.stderr)
189
+ raise typer.Exit(code=EXIT_SCAN_NOT_FOUND)
190
+ elif resp.status_code != 200:
191
+ print(f"Error: {resp.text}", file=sys.stderr)
192
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
193
+
194
+ data = resp.json()
195
+ return write_payload(data, fmt, quiet)
196
+
197
+ handle_scan_status_interactive(scan_id, headers, fmt, quiet)
198
+
199
+ @app.command()
200
+ def version():
201
+ """Show version and exit."""
202
+ typer.echo("0.1.0")
203
+
204
+ @app.command()
205
+ def usage(
206
+ api_key: str = typer.Option(None, "--api-key", "-k", envvar="RAFTER_API_KEY", help="API key or RAFTER_API_KEY env var"),
207
+ ):
208
+ key = resolve_key(api_key)
209
+ headers = {"x-api-key": key}
210
+ resp = requests.get(f"{API_BASE}/static/usage", headers=headers)
211
+
212
+ if resp.status_code != 200:
213
+ print(f"Error: {resp.text}", file=sys.stderr)
214
+ raise typer.Exit(code=EXIT_GENERAL_ERROR)
215
+
216
+ print(json.dumps(resp.json(), indent=2))
217
+
218
+ if __name__ == "__main__":
219
+ app()