@rafter-security/cli 0.1.0
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.
- package/README.md +108 -0
- package/dist/index.js +281 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# @rafter-security/cli
|
|
2
|
+
|
|
3
|
+
A Node.js CLI for Rafter Security that supports npm, pnpm, and yarn package managers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Using npm
|
|
9
|
+
npm install -g @rafter-security/cli
|
|
10
|
+
|
|
11
|
+
# Using pnpm
|
|
12
|
+
pnpm add -g @rafter-security/cli
|
|
13
|
+
|
|
14
|
+
# Using yarn
|
|
15
|
+
yarn global add @rafter-security/cli
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Set your API key
|
|
22
|
+
export RAFTER_API_KEY="your-api-key-here"
|
|
23
|
+
|
|
24
|
+
# Run a security scan
|
|
25
|
+
rafter run
|
|
26
|
+
|
|
27
|
+
# Get scan results
|
|
28
|
+
rafter get <scan-id>
|
|
29
|
+
|
|
30
|
+
# Check API usage
|
|
31
|
+
rafter usage
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
### `rafter run [options]`
|
|
37
|
+
|
|
38
|
+
Trigger a new security scan for your repository.
|
|
39
|
+
|
|
40
|
+
**Options:**
|
|
41
|
+
- `-r, --repo <repo>` - Repository in format `org/repo` (default: auto-detected)
|
|
42
|
+
- `-b, --branch <branch>` - Branch name (default: auto-detected)
|
|
43
|
+
- `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
|
|
44
|
+
- `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
|
|
45
|
+
- `--skip-interactive` - Don't wait for scan completion
|
|
46
|
+
- `--quiet` - Suppress status messages
|
|
47
|
+
|
|
48
|
+
**Examples:**
|
|
49
|
+
```bash
|
|
50
|
+
# Basic scan with auto-detection
|
|
51
|
+
rafter run
|
|
52
|
+
|
|
53
|
+
# Scan specific repo/branch
|
|
54
|
+
rafter run --repo myorg/myrepo --branch feature-branch
|
|
55
|
+
|
|
56
|
+
# Non-interactive scan
|
|
57
|
+
rafter run --skip-interactive
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `rafter get <scan-id> [options]`
|
|
61
|
+
|
|
62
|
+
Retrieve results from a completed scan.
|
|
63
|
+
|
|
64
|
+
**Options:**
|
|
65
|
+
- `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
|
|
66
|
+
- `-f, --format <format>` - Output format: `json` or `md` (default: `json`)
|
|
67
|
+
- `--interactive` - Poll until scan completes
|
|
68
|
+
- `--quiet` - Suppress status messages
|
|
69
|
+
|
|
70
|
+
**Examples:**
|
|
71
|
+
```bash
|
|
72
|
+
# Get scan results
|
|
73
|
+
rafter get <scan-id>
|
|
74
|
+
|
|
75
|
+
# Wait for scan completion
|
|
76
|
+
rafter get <scan-id> --interactive
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### `rafter usage [options]`
|
|
80
|
+
|
|
81
|
+
Check your API quota and usage.
|
|
82
|
+
|
|
83
|
+
**Options:**
|
|
84
|
+
- `-k, --api-key <key>` - API key (or set `RAFTER_API_KEY` env var)
|
|
85
|
+
|
|
86
|
+
**Example:**
|
|
87
|
+
```bash
|
|
88
|
+
rafter usage
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
### Environment Variables
|
|
94
|
+
|
|
95
|
+
- `RAFTER_API_KEY` - Your Rafter API key (alternative to `--api-key` flag)
|
|
96
|
+
|
|
97
|
+
### Git Auto-Detection
|
|
98
|
+
|
|
99
|
+
The CLI automatically detects your repository and branch from the current Git repository:
|
|
100
|
+
|
|
101
|
+
1. **Repository**: Extracted from Git remote URL
|
|
102
|
+
2. **Branch**: Current branch name, or `main` if on detached HEAD
|
|
103
|
+
|
|
104
|
+
**Note**: The CLI only scans remote repositories, not your current local branch.
|
|
105
|
+
|
|
106
|
+
## Documentation
|
|
107
|
+
|
|
108
|
+
For comprehensive documentation, API reference, and examples, see [https://docs.rafter.so](https://docs.rafter.so).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import * as dotenv from "dotenv";
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
dotenv.config();
|
|
8
|
+
const program = new Command()
|
|
9
|
+
.name("rafter")
|
|
10
|
+
.description("Rafter CLI")
|
|
11
|
+
.version("0.1.0");
|
|
12
|
+
const API = "https://rafter.so/api/";
|
|
13
|
+
// Exit codes
|
|
14
|
+
const EXIT_SUCCESS = 0;
|
|
15
|
+
const EXIT_GENERAL_ERROR = 1;
|
|
16
|
+
const EXIT_SCAN_NOT_FOUND = 2;
|
|
17
|
+
const EXIT_QUOTA_EXHAUSTED = 3;
|
|
18
|
+
function resolveKey(cliKey) {
|
|
19
|
+
if (cliKey)
|
|
20
|
+
return cliKey;
|
|
21
|
+
if (process.env.RAFTER_API_KEY)
|
|
22
|
+
return process.env.RAFTER_API_KEY;
|
|
23
|
+
console.error("No API key provided. Use --api-key or set RAFTER_API_KEY");
|
|
24
|
+
process.exit(EXIT_GENERAL_ERROR);
|
|
25
|
+
}
|
|
26
|
+
function writePayload(data, fmt, quiet) {
|
|
27
|
+
const payload = fmt === "md" && data.markdown ? data.markdown : JSON.stringify(data, null, quiet ? 0 : 2);
|
|
28
|
+
// Stream to stdout for pipelines
|
|
29
|
+
process.stdout.write(payload);
|
|
30
|
+
return EXIT_SUCCESS;
|
|
31
|
+
}
|
|
32
|
+
function git(cmd) {
|
|
33
|
+
return execSync(`git ${cmd}`, { stdio: ["ignore", "pipe", "ignore"] })
|
|
34
|
+
.toString()
|
|
35
|
+
.trim();
|
|
36
|
+
}
|
|
37
|
+
function safeBranch(gitFn) {
|
|
38
|
+
try {
|
|
39
|
+
return gitFn("symbolic-ref --quiet --short HEAD");
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return gitFn("rev-parse --short HEAD");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function parseRemote(url) {
|
|
46
|
+
url = url.replace(/^(https?:\/\/|git@)/, "").replace(":", "/");
|
|
47
|
+
if (url.endsWith(".git"))
|
|
48
|
+
url = url.slice(0, -4);
|
|
49
|
+
const parts = url.split("/");
|
|
50
|
+
return parts.slice(-2).join("/"); // owner/repo
|
|
51
|
+
}
|
|
52
|
+
function detectRepo(opts) {
|
|
53
|
+
if (opts.repo && opts.branch)
|
|
54
|
+
return opts;
|
|
55
|
+
const repoEnv = process.env.GITHUB_REPOSITORY || process.env.CI_REPOSITORY;
|
|
56
|
+
const branchEnv = process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_BRANCH || process.env.CI_BRANCH;
|
|
57
|
+
let repoSlug = opts.repo || repoEnv;
|
|
58
|
+
let branch = opts.branch || branchEnv;
|
|
59
|
+
try {
|
|
60
|
+
if (!repoSlug || !branch) {
|
|
61
|
+
if (git("rev-parse --is-inside-work-tree") !== "true")
|
|
62
|
+
throw new Error("not a repo");
|
|
63
|
+
if (!repoSlug)
|
|
64
|
+
repoSlug = parseRemote(git("remote get-url origin"));
|
|
65
|
+
if (!branch) {
|
|
66
|
+
try {
|
|
67
|
+
branch = safeBranch(git);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
branch = "main";
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if ((!opts.repo || !opts.branch) && !opts.quiet) {
|
|
75
|
+
console.error(`Repo auto-detected: ${repoSlug} @ ${branch} (note: scanning remote)`);
|
|
76
|
+
}
|
|
77
|
+
return { repo: repoSlug, branch };
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
throw new Error("Could not auto-detect Git repository. Please pass --repo and --branch explicitly.");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function handleScanStatus(scan_id, headers, fmt, quiet) {
|
|
84
|
+
// First poll
|
|
85
|
+
let poll;
|
|
86
|
+
try {
|
|
87
|
+
poll = await axios.get(`${API}/static/scan`, { params: { scan_id, format: fmt }, headers });
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
if (e.response?.status === 404) {
|
|
91
|
+
console.error(`Scan '${scan_id}' not found`);
|
|
92
|
+
return EXIT_SCAN_NOT_FOUND;
|
|
93
|
+
}
|
|
94
|
+
console.error(`Error: ${e.response?.data || e.message}`);
|
|
95
|
+
return EXIT_GENERAL_ERROR;
|
|
96
|
+
}
|
|
97
|
+
let status = poll.data.status;
|
|
98
|
+
if (["queued", "pending", "processing"].includes(status)) {
|
|
99
|
+
if (!quiet) {
|
|
100
|
+
const spinner = ora("Waiting for scan to complete... (this could take several minutes)").start();
|
|
101
|
+
while (["queued", "pending", "processing"].includes(status)) {
|
|
102
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
103
|
+
poll = await axios.get(`${API}/static/scan`, { params: { scan_id, format: fmt }, headers });
|
|
104
|
+
status = poll.data.status;
|
|
105
|
+
if (status === "completed") {
|
|
106
|
+
spinner.succeed("Scan completed");
|
|
107
|
+
return writePayload(poll.data, fmt, quiet);
|
|
108
|
+
}
|
|
109
|
+
else if (status === "failed") {
|
|
110
|
+
spinner.fail("Scan failed");
|
|
111
|
+
return EXIT_GENERAL_ERROR;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
console.error(`Scan status: ${status}`);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
while (["queued", "pending", "processing"].includes(status)) {
|
|
118
|
+
await new Promise((r) => setTimeout(r, 10000));
|
|
119
|
+
poll = await axios.get(`${API}/static/scan`, { params: { scan_id, format: fmt }, headers });
|
|
120
|
+
status = poll.data.status;
|
|
121
|
+
if (status === "completed") {
|
|
122
|
+
return writePayload(poll.data, fmt, quiet);
|
|
123
|
+
}
|
|
124
|
+
else if (status === "failed") {
|
|
125
|
+
return EXIT_GENERAL_ERROR;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (status === "completed") {
|
|
131
|
+
if (!quiet) {
|
|
132
|
+
console.error("Scan completed");
|
|
133
|
+
}
|
|
134
|
+
return writePayload(poll.data, fmt, quiet);
|
|
135
|
+
}
|
|
136
|
+
else if (status === "failed") {
|
|
137
|
+
console.error("Scan failed");
|
|
138
|
+
return EXIT_GENERAL_ERROR;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
if (!quiet) {
|
|
142
|
+
console.error(`Scan status: ${status}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return writePayload(poll.data, fmt, quiet);
|
|
146
|
+
}
|
|
147
|
+
program
|
|
148
|
+
.name("rafter")
|
|
149
|
+
.description("Rafter CLI");
|
|
150
|
+
program
|
|
151
|
+
.command("run")
|
|
152
|
+
.option("-r, --repo <repo>", "org/repo (default: current)")
|
|
153
|
+
.option("-b, --branch <branch>", "branch (default: current else main)")
|
|
154
|
+
.option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
|
|
155
|
+
.option("-f, --format <format>", "json | md", "json")
|
|
156
|
+
.option("--skip-interactive", "do not wait for scan to complete")
|
|
157
|
+
.option("--quiet", "suppress status messages")
|
|
158
|
+
.action(async (opts) => {
|
|
159
|
+
const key = resolveKey(opts.apiKey);
|
|
160
|
+
let repo, branch;
|
|
161
|
+
try {
|
|
162
|
+
({ repo, branch } = detectRepo({ repo: opts.repo, branch: opts.branch, quiet: opts.quiet }));
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
if (e instanceof Error) {
|
|
166
|
+
console.error(e.message);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
console.error(e);
|
|
170
|
+
}
|
|
171
|
+
process.exit(EXIT_GENERAL_ERROR);
|
|
172
|
+
}
|
|
173
|
+
if (!opts.quiet) {
|
|
174
|
+
const spinner = ora("Submitting scan").start();
|
|
175
|
+
try {
|
|
176
|
+
const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch }, { headers: { "x-api-key": key } });
|
|
177
|
+
spinner.succeed(`Scan ID: ${data.scan_id}`);
|
|
178
|
+
if (opts.skipInteractive)
|
|
179
|
+
return;
|
|
180
|
+
const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format, opts.quiet);
|
|
181
|
+
process.exit(exitCode);
|
|
182
|
+
}
|
|
183
|
+
catch (e) {
|
|
184
|
+
spinner.fail("Request failed");
|
|
185
|
+
if (e.response?.status === 429) {
|
|
186
|
+
console.error("Quota exhausted");
|
|
187
|
+
process.exit(EXIT_QUOTA_EXHAUSTED);
|
|
188
|
+
}
|
|
189
|
+
else if (e.response?.data) {
|
|
190
|
+
console.error(e.response.data);
|
|
191
|
+
}
|
|
192
|
+
else if (e instanceof Error) {
|
|
193
|
+
console.error(e.message);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
console.error(e);
|
|
197
|
+
}
|
|
198
|
+
process.exit(EXIT_GENERAL_ERROR);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
try {
|
|
203
|
+
const { data } = await axios.post(`${API}/static/scan`, { repository_name: repo, branch_name: branch }, { headers: { "x-api-key": key } });
|
|
204
|
+
if (opts.skipInteractive)
|
|
205
|
+
return;
|
|
206
|
+
const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format, opts.quiet);
|
|
207
|
+
process.exit(exitCode);
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
if (e.response?.status === 429) {
|
|
211
|
+
process.exit(EXIT_QUOTA_EXHAUSTED);
|
|
212
|
+
}
|
|
213
|
+
else if (e.response?.data) {
|
|
214
|
+
console.error(e.response.data);
|
|
215
|
+
}
|
|
216
|
+
else if (e instanceof Error) {
|
|
217
|
+
console.error(e.message);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
console.error(e);
|
|
221
|
+
}
|
|
222
|
+
process.exit(EXIT_GENERAL_ERROR);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
program
|
|
227
|
+
.command("get")
|
|
228
|
+
.argument("<scan_id>")
|
|
229
|
+
.option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
|
|
230
|
+
.option("-f, --format <format>", "json | md", "json")
|
|
231
|
+
.option("--interactive", "poll until done")
|
|
232
|
+
.option("--quiet", "suppress status messages")
|
|
233
|
+
.action(async (scan_id, opts) => {
|
|
234
|
+
const key = resolveKey(opts.apiKey);
|
|
235
|
+
if (!opts.interactive) {
|
|
236
|
+
try {
|
|
237
|
+
const { data } = await axios.get(`${API}/static/scan`, { params: { scan_id, format: opts.format }, headers: { "x-api-key": key } });
|
|
238
|
+
const exitCode = writePayload(data, opts.format, opts.quiet);
|
|
239
|
+
process.exit(exitCode);
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
if (e.response?.status === 404) {
|
|
243
|
+
console.error(`Scan '${scan_id}' not found`);
|
|
244
|
+
process.exit(EXIT_SCAN_NOT_FOUND);
|
|
245
|
+
}
|
|
246
|
+
else if (e.response?.data) {
|
|
247
|
+
console.error(e.response.data);
|
|
248
|
+
}
|
|
249
|
+
else if (e instanceof Error) {
|
|
250
|
+
console.error(e.message);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
console.error(e);
|
|
254
|
+
}
|
|
255
|
+
process.exit(EXIT_GENERAL_ERROR);
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const exitCode = await handleScanStatus(scan_id, { "x-api-key": key }, opts.format, opts.quiet);
|
|
260
|
+
process.exit(exitCode);
|
|
261
|
+
});
|
|
262
|
+
program
|
|
263
|
+
.command("usage")
|
|
264
|
+
.option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
|
|
265
|
+
.action(async (opts) => {
|
|
266
|
+
const key = resolveKey(opts.apiKey);
|
|
267
|
+
try {
|
|
268
|
+
const { data } = await axios.get(`${API}/static/usage`, { headers: { "x-api-key": key } });
|
|
269
|
+
console.log(JSON.stringify(data, null, 2));
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
if (e.response?.data) {
|
|
273
|
+
console.error(e.response.data);
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
console.error(e.message);
|
|
277
|
+
}
|
|
278
|
+
process.exit(EXIT_GENERAL_ERROR);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rafter-security/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"rafter": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc -p tsconfig.json",
|
|
11
|
+
"prepublishOnly": "pnpm run build",
|
|
12
|
+
"test": "vitest"
|
|
13
|
+
},
|
|
14
|
+
"engines": { "node": ">=18" },
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"commander": "^11.1.0",
|
|
18
|
+
"axios": "^1.6.8",
|
|
19
|
+
"dotenv": "^16.4.5",
|
|
20
|
+
"chalk": "^5.3.0",
|
|
21
|
+
"ora": "^7.0.1"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"tsx": "^4.7.0",
|
|
25
|
+
"typescript": "^5.4.5",
|
|
26
|
+
"@types/node": "^20.11.30",
|
|
27
|
+
"vitest": "^1.5.0"
|
|
28
|
+
}
|
|
29
|
+
}
|