@rafter-security/cli 0.5.3 → 0.5.5

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.
@@ -1,4 +1,5 @@
1
1
  import fs from "fs";
2
+ import os from "os";
2
3
  import path from "path";
3
4
  import https from "https";
4
5
  import { exec, execSync } from "child_process";
@@ -6,7 +7,7 @@ import { promisify } from "util";
6
7
  import { getBinDir } from "../core/config-defaults.js";
7
8
  import * as tar from "tar";
8
9
  const execAsync = promisify(exec);
9
- const GITLEAKS_VERSION = "8.18.2";
10
+ export const GITLEAKS_VERSION = "8.18.2";
10
11
  export class BinaryManager {
11
12
  constructor() {
12
13
  this.binDir = getBinDir();
@@ -145,9 +146,11 @@ export class BinaryManager {
145
146
  return lines.join("\n");
146
147
  }
147
148
  /**
148
- * Download and install Gitleaks
149
+ * Download and install Gitleaks.
150
+ * @param onProgress Optional progress callback.
151
+ * @param version Gitleaks version to install (defaults to GITLEAKS_VERSION).
149
152
  */
150
- async downloadGitleaks(onProgress) {
153
+ async downloadGitleaks(onProgress, version = GITLEAKS_VERSION) {
151
154
  const log = onProgress || (() => { });
152
155
  // Check platform support
153
156
  if (!this.isPlatformSupported()) {
@@ -159,8 +162,8 @@ export class BinaryManager {
159
162
  }
160
163
  const platform = this.getPlatformString();
161
164
  const arch = this.getArchString();
162
- const url = this.getDownloadUrl(platform, arch);
163
- log(`Downloading Gitleaks v${GITLEAKS_VERSION} for ${platform}/${arch}...`);
165
+ const url = this.getDownloadUrl(platform, arch, version);
166
+ log(`Downloading Gitleaks v${version} for ${platform}/${arch}...`);
164
167
  log(` URL: ${url}`);
165
168
  const archivePath = path.join(this.binDir, platform === "windows" ? "gitleaks.zip" : "gitleaks.tar.gz");
166
169
  try {
@@ -172,8 +175,7 @@ export class BinaryManager {
172
175
  // Extract binary
173
176
  log("Extracting binary...");
174
177
  if (platform === "windows") {
175
- // For Windows, we'd need unzip - for now just error
176
- throw new Error("Windows support coming soon");
178
+ await this.extractZip(archivePath);
177
179
  }
178
180
  else {
179
181
  await this.extractTarball(archivePath);
@@ -255,15 +257,15 @@ export class BinaryManager {
255
257
  throw new Error(`Unsupported architecture: ${arch}`);
256
258
  }
257
259
  /**
258
- * Get download URL for platform/arch
260
+ * Get download URL for platform/arch/version
259
261
  */
260
- getDownloadUrl(platform, arch) {
261
- const baseUrl = `https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}`;
262
+ getDownloadUrl(platform, arch, version = GITLEAKS_VERSION) {
263
+ const baseUrl = `https://github.com/gitleaks/gitleaks/releases/download/v${version}`;
262
264
  if (platform === "windows") {
263
- return `${baseUrl}/gitleaks_${GITLEAKS_VERSION}_windows_${arch}.zip`;
265
+ return `${baseUrl}/gitleaks_${version}_windows_${arch}.zip`;
264
266
  }
265
267
  else {
266
- return `${baseUrl}/gitleaks_${GITLEAKS_VERSION}_${platform}_${arch}.tar.gz`;
268
+ return `${baseUrl}/gitleaks_${version}_${platform}_${arch}.tar.gz`;
267
269
  }
268
270
  }
269
271
  /**
@@ -320,13 +322,49 @@ export class BinaryManager {
320
322
  });
321
323
  }
322
324
  /**
323
- * Extract tarballbinary only, strip packaging extras (LICENSE, README.md)
325
+ * Extract zip (Windows) uses PowerShell's Expand-Archive, then copies
326
+ * only the gitleaks.exe binary to binDir. Cleans up the temp extract dir.
327
+ */
328
+ async extractZip(zipPath) {
329
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "rafter-gitleaks-"));
330
+ try {
331
+ // PowerShell 5+ ships on all supported Windows versions
332
+ await execAsync(`powershell -NoProfile -Command "Expand-Archive -Force -LiteralPath '${zipPath}' -DestinationPath '${tempDir}'"`, { timeout: 30000 });
333
+ // Find gitleaks.exe — may be at root or inside a subdirectory
334
+ const findBinary = (dir) => {
335
+ for (const entry of fs.readdirSync(dir)) {
336
+ const full = path.join(dir, entry);
337
+ if (entry === "gitleaks.exe")
338
+ return full;
339
+ if (fs.statSync(full).isDirectory()) {
340
+ const found = findBinary(full);
341
+ if (found)
342
+ return found;
343
+ }
344
+ }
345
+ return null;
346
+ };
347
+ const found = findBinary(tempDir);
348
+ if (!found)
349
+ throw new Error("gitleaks.exe not found in archive");
350
+ fs.copyFileSync(found, path.join(this.binDir, "gitleaks.exe"));
351
+ }
352
+ finally {
353
+ fs.rmSync(tempDir, { recursive: true, force: true });
354
+ }
355
+ }
356
+ /**
357
+ * Extract tarball — binary only, strip packaging extras (LICENSE, README.md).
358
+ *
359
+ * The gitleaks release tarball has all files at the archive root (no top-level
360
+ * directory), so strip: 0 (the default). With strip: 1, node-tar reduces the
361
+ * single-component paths to empty strings; the filter never matches "gitleaks"
362
+ * and nothing is extracted. The filter alone is sufficient.
324
363
  */
325
364
  async extractTarball(tarballPath) {
326
365
  await tar.extract({
327
366
  file: tarballPath,
328
367
  cwd: this.binDir,
329
- strip: 1,
330
368
  filter: (p) => {
331
369
  const base = path.basename(p);
332
370
  return base === "gitleaks" || base === "gitleaks.exe";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafter-security/cli",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "rafter": "./dist/index.js"
@@ -0,0 +1,60 @@
1
+ #!/bin/bash
2
+ # Rafter Security Pre-Push Hook
3
+ # Scans commits being pushed for secrets
4
+
5
+ # Colors for output
6
+ RED='\033[0;31m'
7
+ YELLOW='\033[1;33m'
8
+ GREEN='\033[0;32m'
9
+ NC='\033[0m' # No Color
10
+
11
+ # Check if rafter is installed
12
+ if ! command -v rafter &> /dev/null; then
13
+ echo -e "${YELLOW}⚠️ Warning: rafter CLI not found in PATH${NC}"
14
+ echo " Install: npm install -g @rafter-security/cli"
15
+ echo " Skipping secret scan..."
16
+ exit 0
17
+ fi
18
+
19
+ ZERO_SHA="0000000000000000000000000000000000000000"
20
+ FOUND_SECRETS=0
21
+
22
+ while read local_ref local_sha remote_ref remote_sha; do
23
+ # Skip branch deletions
24
+ if [ "$local_sha" = "$ZERO_SHA" ]; then
25
+ continue
26
+ fi
27
+
28
+ if [ "$remote_sha" = "$ZERO_SHA" ]; then
29
+ # New branch — scan all commits on this branch not on any remote branch
30
+ ref_arg=$(git rev-list --max-parents=0 "$local_sha" 2>/dev/null | head -1)
31
+ if [ -z "$ref_arg" ]; then
32
+ ref_arg="$local_sha^"
33
+ fi
34
+ else
35
+ # Existing branch — scan only new commits
36
+ ref_arg="$remote_sha"
37
+ fi
38
+
39
+ echo "🔍 Rafter: Scanning commits being pushed ($local_ref)..."
40
+
41
+ rafter agent scan --diff "$ref_arg" --quiet
42
+ EXIT_CODE=$?
43
+
44
+ if [ $EXIT_CODE -ne 0 ]; then
45
+ FOUND_SECRETS=1
46
+ fi
47
+ done
48
+
49
+ if [ $FOUND_SECRETS -ne 0 ]; then
50
+ echo -e "${RED}❌ Push blocked: Secrets detected in commits being pushed${NC}"
51
+ echo ""
52
+ echo " Run: rafter agent scan --diff <remote-sha>"
53
+ echo " To see details and remediate."
54
+ echo ""
55
+ echo " To bypass (NOT recommended): git push --no-verify"
56
+ exit 1
57
+ fi
58
+
59
+ echo -e "${GREEN}✓ No secrets detected${NC}"
60
+ exit 0