@rafter-security/cli 0.1.0 → 0.4.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.
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Default secret detection patterns
3
+ * Based on common secret formats and Gitleaks rules
4
+ */
5
+ export const DEFAULT_SECRET_PATTERNS = [
6
+ // AWS
7
+ {
8
+ name: "AWS Access Key ID",
9
+ regex: "(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}",
10
+ severity: "critical",
11
+ description: "AWS Access Key ID detected"
12
+ },
13
+ {
14
+ name: "AWS Secret Access Key",
15
+ regex: "(?i)aws(.{0,20})?['\"][0-9a-zA-Z\\/+]{40}['\"]",
16
+ severity: "critical",
17
+ description: "AWS Secret Access Key detected"
18
+ },
19
+ // GitHub
20
+ {
21
+ name: "GitHub Personal Access Token",
22
+ regex: "ghp_[0-9a-zA-Z]{36}",
23
+ severity: "critical",
24
+ description: "GitHub Personal Access Token detected"
25
+ },
26
+ {
27
+ name: "GitHub OAuth Token",
28
+ regex: "gho_[0-9a-zA-Z]{36}",
29
+ severity: "critical",
30
+ description: "GitHub OAuth Token detected"
31
+ },
32
+ {
33
+ name: "GitHub App Token",
34
+ regex: "(ghu|ghs)_[0-9a-zA-Z]{36}",
35
+ severity: "critical",
36
+ description: "GitHub App Token detected"
37
+ },
38
+ {
39
+ name: "GitHub Refresh Token",
40
+ regex: "ghr_[0-9a-zA-Z]{76}",
41
+ severity: "critical",
42
+ description: "GitHub Refresh Token detected"
43
+ },
44
+ // Google
45
+ {
46
+ name: "Google API Key",
47
+ regex: "AIza[0-9A-Za-z\\-_]{35}",
48
+ severity: "critical",
49
+ description: "Google API Key detected"
50
+ },
51
+ {
52
+ name: "Google OAuth",
53
+ regex: "[0-9]+-[0-9A-Za-z_]{32}\\.apps\\.googleusercontent\\.com",
54
+ severity: "critical",
55
+ description: "Google OAuth Client ID detected"
56
+ },
57
+ // Slack
58
+ {
59
+ name: "Slack Token",
60
+ regex: "xox[baprs]-([0-9a-zA-Z]{10,48})",
61
+ severity: "critical",
62
+ description: "Slack Token detected"
63
+ },
64
+ {
65
+ name: "Slack Webhook",
66
+ regex: "https://hooks\\.slack\\.com/services/T[a-zA-Z0-9_]{8}/B[a-zA-Z0-9_]{8}/[a-zA-Z0-9_]{24}",
67
+ severity: "high",
68
+ description: "Slack Webhook URL detected"
69
+ },
70
+ // Stripe
71
+ {
72
+ name: "Stripe API Key",
73
+ regex: "(?i)sk_live_[0-9a-zA-Z]{24}",
74
+ severity: "critical",
75
+ description: "Stripe Live API Key detected"
76
+ },
77
+ {
78
+ name: "Stripe Restricted API Key",
79
+ regex: "(?i)rk_live_[0-9a-zA-Z]{24}",
80
+ severity: "critical",
81
+ description: "Stripe Restricted API Key detected"
82
+ },
83
+ // Twilio
84
+ {
85
+ name: "Twilio API Key",
86
+ regex: "SK[0-9a-fA-F]{32}",
87
+ severity: "critical",
88
+ description: "Twilio API Key detected"
89
+ },
90
+ // Generic patterns
91
+ {
92
+ name: "Generic API Key",
93
+ regex: "(?i)(api[_-]?key|apikey)[\\s]*[:=][\\s]*['\"]?[0-9a-zA-Z\\-_]{16,}['\"]?",
94
+ severity: "high",
95
+ description: "Generic API key pattern detected"
96
+ },
97
+ {
98
+ name: "Generic Secret",
99
+ regex: "(?i)(secret|password|passwd|pwd)[\\s]*[:=][\\s]*['\"]?[0-9a-zA-Z\\-_!@#$%^&*()]{8,}['\"]?",
100
+ severity: "high",
101
+ description: "Generic secret pattern detected"
102
+ },
103
+ {
104
+ name: "Private Key",
105
+ regex: "-----BEGIN (RSA |DSA |EC |OPENSSH )?PRIVATE KEY-----",
106
+ severity: "critical",
107
+ description: "Private key detected"
108
+ },
109
+ {
110
+ name: "Bearer Token",
111
+ regex: "(?i)bearer[\\s]+[a-zA-Z0-9\\-_\\.=]+",
112
+ severity: "high",
113
+ description: "Bearer token detected"
114
+ },
115
+ // Database connection strings
116
+ {
117
+ name: "Database Connection String",
118
+ regex: "(?i)(postgres|mysql|mongodb)://[^\\s]+:[^\\s]+@[^\\s]+",
119
+ severity: "critical",
120
+ description: "Database connection string with credentials detected"
121
+ },
122
+ // JWT
123
+ {
124
+ name: "JSON Web Token",
125
+ regex: "eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}",
126
+ severity: "high",
127
+ description: "JWT token detected"
128
+ },
129
+ // npm token
130
+ {
131
+ name: "npm Access Token",
132
+ regex: "npm_[A-Za-z0-9]{36}",
133
+ severity: "critical",
134
+ description: "npm access token detected"
135
+ },
136
+ // PyPI token
137
+ {
138
+ name: "PyPI Token",
139
+ regex: "pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\\-_]{50,}",
140
+ severity: "critical",
141
+ description: "PyPI API token detected"
142
+ }
143
+ ];
144
+ /**
145
+ * Get patterns by severity level
146
+ */
147
+ export function getPatternsBySeverity(severity) {
148
+ return DEFAULT_SECRET_PATTERNS.filter(p => p.severity === severity);
149
+ }
150
+ /**
151
+ * Get all critical patterns
152
+ */
153
+ export function getCriticalPatterns() {
154
+ return getPatternsBySeverity("critical");
155
+ }
@@ -0,0 +1,20 @@
1
+ export const API = "https://rafter.so/api/";
2
+ // Exit codes
3
+ export const EXIT_SUCCESS = 0;
4
+ export const EXIT_GENERAL_ERROR = 1;
5
+ export const EXIT_SCAN_NOT_FOUND = 2;
6
+ export const EXIT_QUOTA_EXHAUSTED = 3;
7
+ export function resolveKey(cliKey) {
8
+ if (cliKey)
9
+ return cliKey;
10
+ if (process.env.RAFTER_API_KEY)
11
+ return process.env.RAFTER_API_KEY;
12
+ console.error("No API key provided. Use --api-key or set RAFTER_API_KEY");
13
+ process.exit(EXIT_GENERAL_ERROR);
14
+ }
15
+ export function writePayload(data, fmt, quiet) {
16
+ const payload = fmt === "md" && data.markdown ? data.markdown : JSON.stringify(data, null, quiet ? 0 : 2);
17
+ // Stream to stdout for pipelines
18
+ process.stdout.write(payload);
19
+ return EXIT_SUCCESS;
20
+ }
@@ -0,0 +1,243 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import https from "https";
4
+ import { exec } from "child_process";
5
+ import { promisify } from "util";
6
+ import { getBinDir } from "../core/config-defaults.js";
7
+ import * as tar from "tar";
8
+ const execAsync = promisify(exec);
9
+ const GITLEAKS_VERSION = "8.18.2";
10
+ export class BinaryManager {
11
+ constructor() {
12
+ this.binDir = getBinDir();
13
+ }
14
+ /**
15
+ * Check if current platform is supported
16
+ */
17
+ isPlatformSupported() {
18
+ const platform = process.platform;
19
+ const arch = process.arch;
20
+ // Supported platforms
21
+ const supported = [
22
+ "darwin-x64",
23
+ "darwin-arm64",
24
+ "linux-x64",
25
+ "linux-arm64",
26
+ "win32-x64"
27
+ ];
28
+ return supported.includes(`${platform}-${arch}`);
29
+ }
30
+ /**
31
+ * Get platform info for display
32
+ */
33
+ getPlatformInfo() {
34
+ return {
35
+ platform: process.platform,
36
+ arch: process.arch,
37
+ supported: this.isPlatformSupported()
38
+ };
39
+ }
40
+ /**
41
+ * Get Gitleaks binary path
42
+ */
43
+ getGitleaksPath() {
44
+ const platform = process.platform;
45
+ const ext = platform === "win32" ? ".exe" : "";
46
+ return path.join(this.binDir, `gitleaks${ext}`);
47
+ }
48
+ /**
49
+ * Check if Gitleaks is installed
50
+ */
51
+ isGitleaksInstalled() {
52
+ const gitleaksPath = this.getGitleaksPath();
53
+ return fs.existsSync(gitleaksPath);
54
+ }
55
+ /**
56
+ * Verify Gitleaks binary works
57
+ */
58
+ async verifyGitleaks() {
59
+ if (!this.isGitleaksInstalled()) {
60
+ return false;
61
+ }
62
+ try {
63
+ const { stdout } = await execAsync(`"${this.getGitleaksPath()}" version`, {
64
+ timeout: 5000
65
+ });
66
+ return stdout.includes("gitleaks version");
67
+ }
68
+ catch {
69
+ return false;
70
+ }
71
+ }
72
+ /**
73
+ * Download and install Gitleaks
74
+ */
75
+ async downloadGitleaks(onProgress) {
76
+ const log = onProgress || (() => { });
77
+ // Check platform support
78
+ if (!this.isPlatformSupported()) {
79
+ throw new Error(`Gitleaks not available for ${process.platform}/${process.arch}`);
80
+ }
81
+ // Ensure bin directory exists
82
+ if (!fs.existsSync(this.binDir)) {
83
+ fs.mkdirSync(this.binDir, { recursive: true });
84
+ }
85
+ const platform = this.getPlatformString();
86
+ const arch = this.getArchString();
87
+ const url = this.getDownloadUrl(platform, arch);
88
+ log(`Downloading Gitleaks v${GITLEAKS_VERSION} for ${platform}/${arch}...`);
89
+ const archivePath = path.join(this.binDir, platform === "windows" ? "gitleaks.zip" : "gitleaks.tar.gz");
90
+ try {
91
+ // Download archive
92
+ await this.downloadFile(url, archivePath, log);
93
+ // Extract binary
94
+ log("Extracting binary...");
95
+ if (platform === "windows") {
96
+ // For Windows, we'd need unzip - for now just error
97
+ throw new Error("Windows support coming soon");
98
+ }
99
+ else {
100
+ await this.extractTarball(archivePath);
101
+ }
102
+ // Make executable (Unix systems)
103
+ if (process.platform !== "win32") {
104
+ await execAsync(`chmod +x "${this.getGitleaksPath()}"`);
105
+ }
106
+ // Verify it works
107
+ const works = await this.verifyGitleaks();
108
+ if (!works) {
109
+ throw new Error("Downloaded binary doesn't execute correctly");
110
+ }
111
+ // Clean up archive
112
+ if (fs.existsSync(archivePath)) {
113
+ fs.unlinkSync(archivePath);
114
+ }
115
+ log("✓ Gitleaks installed successfully");
116
+ }
117
+ catch (e) {
118
+ // Clean up on failure
119
+ if (fs.existsSync(archivePath)) {
120
+ fs.unlinkSync(archivePath);
121
+ }
122
+ const gitleaksPath = this.getGitleaksPath();
123
+ if (fs.existsSync(gitleaksPath)) {
124
+ fs.unlinkSync(gitleaksPath);
125
+ }
126
+ throw e;
127
+ }
128
+ }
129
+ /**
130
+ * Get Gitleaks version
131
+ */
132
+ async getGitleaksVersion() {
133
+ if (!this.isGitleaksInstalled()) {
134
+ return "not installed";
135
+ }
136
+ try {
137
+ const { stdout } = await execAsync(`"${this.getGitleaksPath()}" version`);
138
+ return stdout.trim();
139
+ }
140
+ catch {
141
+ return "unknown";
142
+ }
143
+ }
144
+ /**
145
+ * Get platform string for download URL
146
+ */
147
+ getPlatformString() {
148
+ const platform = process.platform;
149
+ if (platform === "darwin")
150
+ return "darwin";
151
+ if (platform === "win32")
152
+ return "windows";
153
+ if (platform === "linux")
154
+ return "linux";
155
+ throw new Error(`Unsupported platform: ${platform}`);
156
+ }
157
+ /**
158
+ * Get architecture string for download URL
159
+ */
160
+ getArchString() {
161
+ const arch = process.arch;
162
+ if (arch === "x64")
163
+ return "x64";
164
+ if (arch === "arm64")
165
+ return "arm64";
166
+ throw new Error(`Unsupported architecture: ${arch}`);
167
+ }
168
+ /**
169
+ * Get download URL for platform/arch
170
+ */
171
+ getDownloadUrl(platform, arch) {
172
+ const baseUrl = `https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}`;
173
+ if (platform === "windows") {
174
+ return `${baseUrl}/gitleaks_${GITLEAKS_VERSION}_windows_${arch}.zip`;
175
+ }
176
+ else {
177
+ return `${baseUrl}/gitleaks_${GITLEAKS_VERSION}_${platform}_${arch}.tar.gz`;
178
+ }
179
+ }
180
+ /**
181
+ * Download file from URL
182
+ */
183
+ downloadFile(url, dest, onProgress) {
184
+ return new Promise((resolve, reject) => {
185
+ const file = fs.createWriteStream(dest);
186
+ https.get(url, (response) => {
187
+ // Follow redirects
188
+ if (response.statusCode === 302 || response.statusCode === 301) {
189
+ const redirectUrl = response.headers.location;
190
+ if (!redirectUrl) {
191
+ reject(new Error("Redirect without location"));
192
+ return;
193
+ }
194
+ file.close();
195
+ fs.unlinkSync(dest);
196
+ this.downloadFile(redirectUrl, dest, onProgress).then(resolve).catch(reject);
197
+ return;
198
+ }
199
+ if (response.statusCode !== 200) {
200
+ reject(new Error(`Download failed: ${response.statusCode}`));
201
+ return;
202
+ }
203
+ const totalBytes = parseInt(response.headers["content-length"] || "0", 10);
204
+ let downloadedBytes = 0;
205
+ let lastPercent = 0;
206
+ response.on("data", (chunk) => {
207
+ downloadedBytes += chunk.length;
208
+ if (totalBytes > 0) {
209
+ const percent = Math.round((downloadedBytes / totalBytes) * 100);
210
+ if (percent > lastPercent && percent % 10 === 0) {
211
+ onProgress(`Downloading... ${percent}%`);
212
+ lastPercent = percent;
213
+ }
214
+ }
215
+ });
216
+ response.pipe(file);
217
+ file.on("finish", () => {
218
+ file.close();
219
+ resolve();
220
+ });
221
+ file.on("error", (err) => {
222
+ fs.unlinkSync(dest);
223
+ reject(err);
224
+ });
225
+ }).on("error", (err) => {
226
+ if (fs.existsSync(dest)) {
227
+ fs.unlinkSync(dest);
228
+ }
229
+ reject(err);
230
+ });
231
+ });
232
+ }
233
+ /**
234
+ * Extract tarball
235
+ */
236
+ async extractTarball(tarballPath) {
237
+ await tar.extract({
238
+ file: tarballPath,
239
+ cwd: this.binDir,
240
+ strip: 0
241
+ });
242
+ }
243
+ }
@@ -0,0 +1,52 @@
1
+ import { execSync } from "child_process";
2
+ export function git(cmd) {
3
+ return execSync(`git ${cmd}`, { stdio: ["ignore", "pipe", "ignore"] })
4
+ .toString()
5
+ .trim();
6
+ }
7
+ export function safeBranch(gitFn) {
8
+ try {
9
+ return gitFn("symbolic-ref --quiet --short HEAD");
10
+ }
11
+ catch {
12
+ return gitFn("rev-parse --short HEAD");
13
+ }
14
+ }
15
+ export function parseRemote(url) {
16
+ url = url.replace(/^(https?:\/\/|git@)/, "").replace(":", "/");
17
+ if (url.endsWith(".git"))
18
+ url = url.slice(0, -4);
19
+ const parts = url.split("/");
20
+ return parts.slice(-2).join("/"); // owner/repo
21
+ }
22
+ export function detectRepo(opts) {
23
+ if (opts.repo && opts.branch)
24
+ return opts;
25
+ const repoEnv = process.env.GITHUB_REPOSITORY || process.env.CI_REPOSITORY;
26
+ const branchEnv = process.env.GITHUB_REF_NAME || process.env.CI_COMMIT_BRANCH || process.env.CI_BRANCH;
27
+ let repoSlug = opts.repo || repoEnv;
28
+ let branch = opts.branch || branchEnv;
29
+ try {
30
+ if (!repoSlug || !branch) {
31
+ if (git("rev-parse --is-inside-work-tree") !== "true")
32
+ throw new Error("not a repo");
33
+ if (!repoSlug)
34
+ repoSlug = parseRemote(git("remote get-url origin"));
35
+ if (!branch) {
36
+ try {
37
+ branch = safeBranch(git);
38
+ }
39
+ catch {
40
+ branch = "main";
41
+ }
42
+ }
43
+ }
44
+ if ((!opts.repo || !opts.branch) && !opts.quiet) {
45
+ console.error(`Repo auto-detected: ${repoSlug} @ ${branch} (note: scanning remote)`);
46
+ }
47
+ return { repo: repoSlug, branch };
48
+ }
49
+ catch {
50
+ throw new Error("Could not auto-detect Git repository. Please pass --repo and --branch explicitly.");
51
+ }
52
+ }