@rafter-security/cli 0.6.3 → 0.6.4

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.
@@ -39,9 +39,10 @@ export function createAuditCommand() {
39
39
  }
40
40
  console.log(`\nShowing ${entries.length} audit log entries:\n`);
41
41
  for (const entry of entries) {
42
- const timestamp = new Date(entry.timestamp).toLocaleString();
43
- const indicator = getEventIndicator(entry.eventType);
44
- console.log(`${indicator} [${timestamp}] ${entry.eventType}`);
42
+ const timestamp = entry.timestamp ? new Date(entry.timestamp).toLocaleString() : "unknown";
43
+ const eventType = entry.eventType ?? "unknown";
44
+ const indicator = getEventIndicator(eventType);
45
+ console.log(`${indicator} [${timestamp}] ${eventType}`);
45
46
  if (entry.agentType) {
46
47
  console.log(` Agent: ${entry.agentType}`);
47
48
  }
@@ -92,8 +93,8 @@ export function generateShareExcerpt() {
92
93
  }
93
94
  else {
94
95
  for (const entry of entries) {
95
- const ts = entry.timestamp.replace("T", " ").replace(/\.\d+Z$/, "Z");
96
- const eventPad = entry.eventType.padEnd(20);
96
+ const ts = (entry.timestamp ?? "").replace("T", " ").replace(/\.\d+Z$/, "Z");
97
+ const eventPad = (entry.eventType ?? "unknown").padEnd(20);
97
98
  const riskRaw = entry.action?.riskLevel ?? "low";
98
99
  const riskPad = riskRaw.toUpperCase().padEnd(8);
99
100
  const detail = formatShareDetail(entry);
@@ -136,7 +137,7 @@ export function getRiskLevel(config) {
136
137
  export function formatShareDetail(entry) {
137
138
  const action = entry.resolution?.actionTaken ?? "unknown";
138
139
  const suffix = `[${action}]`;
139
- if (entry.eventType === "secret_detected") {
140
+ if ((entry.eventType ?? "unknown") === "secret_detected") {
140
141
  const reason = entry.securityCheck?.reason ?? "";
141
142
  return `${reason} ${suffix}`;
142
143
  }
@@ -5,7 +5,9 @@ import { SkillManager } from "../../utils/skill-manager.js";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
7
  import os from "os";
8
+ import { execSync } from "child_process";
8
9
  import { fileURLToPath } from "url";
10
+ import { createRequire } from "module";
9
11
  import { fmt } from "../../utils/formatter.js";
10
12
  const __filename = fileURLToPath(import.meta.url);
11
13
  const __dirname = path.dirname(__filename);
@@ -585,5 +587,23 @@ export function createInitCommand() {
585
587
  console.log(" - Run: rafter scan local . (test secret scanning)");
586
588
  console.log(" - Configure: rafter agent config show");
587
589
  console.log();
590
+ // Warn if a different rafter version shadows this one on PATH
591
+ try {
592
+ const _require = createRequire(import.meta.url);
593
+ const { version: thisVersion } = _require("../../../package.json");
594
+ const pathVersion = execSync("rafter --version", {
595
+ encoding: "utf-8",
596
+ timeout: 5000,
597
+ stdio: ["pipe", "pipe", "ignore"],
598
+ }).trim();
599
+ if (pathVersion && pathVersion !== thisVersion && !pathVersion.includes(thisVersion)) {
600
+ console.log(fmt.warning(`PATH version mismatch: 'rafter --version' reports ${pathVersion}, but this install is ${thisVersion}.`));
601
+ console.log(fmt.info("Another rafter binary may be shadowing this one. Check: which rafter"));
602
+ console.log();
603
+ }
604
+ }
605
+ catch {
606
+ // Ignore — rafter may not be on PATH yet
607
+ }
588
608
  });
589
609
  }
@@ -34,13 +34,13 @@ export function createStatusCommand() {
34
34
  const localGitleaks = path.join(getBinDir(), "gitleaks");
35
35
  let gitleaksStatus = "not found — run: rafter agent init --with-gitleaks";
36
36
  try {
37
- const ver = execSync("gitleaks version", { timeout: 5000, encoding: "utf-8" }).trim();
37
+ const ver = execSync("gitleaks version", { timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
38
38
  gitleaksStatus = `${ver} (PATH)`;
39
39
  }
40
40
  catch {
41
41
  if (fs.existsSync(localGitleaks)) {
42
42
  try {
43
- const ver = execSync(`"${localGitleaks}" version`, { timeout: 5000, encoding: "utf-8" }).trim();
43
+ const ver = execSync(`"${localGitleaks}" version`, { timeout: 5000, encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] }).trim();
44
44
  gitleaksStatus = `${ver} (local)`;
45
45
  }
46
46
  catch {
@@ -164,7 +164,7 @@ export function createStatusCommand() {
164
164
  for (const e of [...recent].reverse()) {
165
165
  const ts = (e.timestamp ?? "").slice(0, 19).replace("T", " ");
166
166
  const action = e.resolution?.actionTaken ?? "";
167
- console.log(` ${ts} ${e.eventType} [${action}]`);
167
+ console.log(` ${ts} ${e.eventType ?? "unknown"} [${action}]`);
168
168
  }
169
169
  }
170
170
  }
@@ -2,7 +2,7 @@ import { Command } from "commander";
2
2
  import axios from "axios";
3
3
  import ora from "ora";
4
4
  import { detectRepo } from "../../utils/git.js";
5
- import { API, resolveKey, EXIT_GENERAL_ERROR, EXIT_QUOTA_EXHAUSTED, EXIT_INSUFFICIENT_SCOPE, handleScopeError } from "../../utils/api.js";
5
+ import { API, resolveKey, EXIT_GENERAL_ERROR, EXIT_QUOTA_EXHAUSTED, handle403 } from "../../utils/api.js";
6
6
  import { handleScanStatus } from "./scan-status.js";
7
7
  /**
8
8
  * Shared handler for the remote backend scan (used by both `rafter run` and `rafter scan` / `rafter scan remote`).
@@ -34,8 +34,9 @@ export async function runRemoteScan(opts) {
34
34
  }
35
35
  catch (e) {
36
36
  spinner.fail("Request failed");
37
- if (handleScopeError(e)) {
38
- process.exit(EXIT_INSUFFICIENT_SCOPE);
37
+ const forbiddenCode = handle403(e);
38
+ if (forbiddenCode >= 0) {
39
+ process.exit(forbiddenCode);
39
40
  }
40
41
  else if (e.response?.status === 429) {
41
42
  console.error("Quota exhausted");
@@ -62,8 +63,9 @@ export async function runRemoteScan(opts) {
62
63
  process.exit(exitCode);
63
64
  }
64
65
  catch (e) {
65
- if (handleScopeError(e)) {
66
- process.exit(EXIT_INSUFFICIENT_SCOPE);
66
+ const forbiddenCode = handle403(e);
67
+ if (forbiddenCode >= 0) {
68
+ process.exit(forbiddenCode);
67
69
  }
68
70
  else if (e.response?.status === 429) {
69
71
  process.exit(EXIT_QUOTA_EXHAUSTED);
@@ -260,7 +260,11 @@ export class AuditLogger {
260
260
  const lines = content.split("\n").filter(line => line.trim());
261
261
  let entries = lines.map(line => {
262
262
  try {
263
- return JSON.parse(line);
263
+ const parsed = JSON.parse(line);
264
+ // Skip malformed entries missing required fields
265
+ if (!parsed || typeof parsed !== "object" || !parsed.timestamp)
266
+ return null;
267
+ return parsed;
264
268
  }
265
269
  catch {
266
270
  return null;
@@ -152,17 +152,20 @@ export class GitleaksScanner {
152
152
  */
153
153
  getSeverity(ruleID, tags) {
154
154
  const lowerID = ruleID.toLowerCase();
155
- // Critical: Private keys, passwords, database credentials
155
+ // Critical: Private keys, passwords, database credentials, access tokens
156
156
  if (lowerID.includes("private-key") ||
157
157
  lowerID.includes("password") ||
158
158
  lowerID.includes("database") ||
159
- tags.includes("key") && tags.includes("secret")) {
159
+ lowerID.includes("access-token") ||
160
+ lowerID.includes("secret-key") ||
161
+ lowerID.endsWith("-pat") ||
162
+ (tags.includes("key") && tags.includes("secret"))) {
160
163
  return "critical";
161
164
  }
162
- // High: API keys, access tokens
165
+ // High: API keys, generic tokens
163
166
  if (lowerID.includes("api-key") ||
164
- lowerID.includes("access-token") ||
165
- lowerID.includes("secret-key") ||
167
+ lowerID.includes("-token") ||
168
+ lowerID.startsWith("token-") ||
166
169
  tags.includes("api")) {
167
170
  return "high";
168
171
  }
package/dist/utils/api.js CHANGED
@@ -6,13 +6,20 @@ export const EXIT_SCAN_NOT_FOUND = 2;
6
6
  export const EXIT_QUOTA_EXHAUSTED = 3;
7
7
  export const EXIT_INSUFFICIENT_SCOPE = 4;
8
8
  /**
9
- * Detect a 403 scope-enforcement error from the API and print a helpful message.
10
- * Returns true if the error was a scope error (caller should exit), false otherwise.
9
+ * Detect a 403 error from the API and print a helpful message.
10
+ * Returns the appropriate exit code, or -1 if not a 403.
11
11
  */
12
- export function handleScopeError(e) {
12
+ export function handle403(e) {
13
13
  if (!e || e.response?.status !== 403)
14
- return false;
14
+ return -1;
15
15
  const body = e.response?.data;
16
+ if (typeof body === "object" && body?.scan_mode) {
17
+ const mode = body.scan_mode;
18
+ const limit = body.limit ?? "?";
19
+ const used = body.used ?? limit;
20
+ console.error(`Error: ${mode.charAt(0).toUpperCase() + mode.slice(1)} scan limit reached (${used}/${limit} used this billing period).\nUpgrade your plan or wait for your quota to reset.`);
21
+ return EXIT_QUOTA_EXHAUSTED;
22
+ }
16
23
  const msg = typeof body === "string" ? body : body?.error ?? "";
17
24
  if (msg.includes("scope")) {
18
25
  console.error('Error: This API key only has read access.\nTo trigger scans, create a key with "Read & Scan" scope at https://rfrr.co/account');
@@ -20,7 +27,11 @@ export function handleScopeError(e) {
20
27
  else {
21
28
  console.error(`Error: Forbidden (403) — ${msg || "access denied"}`);
22
29
  }
23
- return true;
30
+ return EXIT_INSUFFICIENT_SCOPE;
31
+ }
32
+ /** @deprecated Use handle403 instead */
33
+ export function handleScopeError(e) {
34
+ return handle403(e) >= 0;
24
35
  }
25
36
  export function resolveKey(cliKey) {
26
37
  if (cliKey)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafter-security/cli",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "rafter": "./dist/index.js"
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: rafter
3
3
  description: "Trigger Rafter backend security scans on GitHub repositories. Use when the user asks about SAST, code security analysis, vulnerability scanning, or wants to scan a repo for security issues before merging or deploying. Also use when starting new features or reviewing pull requests."
4
- version: 0.6.3
4
+ version: 0.6.4
5
5
  allowed-tools: [Bash]
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: rafter-agent-security
3
3
  description: "Local security tools for agents: scan files for secrets before commits, audit Claude Code skills before installation, view security audit logs. Use for: pre-commit secret scanning, skill security analysis, audit log review. Note: command blocking is handled automatically by the PreToolUse hook—you do not need to invoke /rafter-bash for normal commands."
4
- version: 0.6.3
4
+ version: 0.6.4
5
5
  disable-model-invocation: true
6
6
  allowed-tools: [Bash, Read, Glob, Grep]
7
7
  ---