@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.
- package/dist/commands/agent/audit.js +7 -6
- package/dist/commands/agent/init.js +20 -0
- package/dist/commands/agent/status.js +3 -3
- package/dist/commands/backend/run.js +7 -5
- package/dist/core/audit-logger.js +5 -1
- package/dist/scanners/gitleaks.js +8 -5
- package/dist/utils/api.js +16 -5
- package/package.json +1 -1
- package/resources/skills/rafter/SKILL.md +1 -1
- package/resources/skills/rafter-agent-security/SKILL.md +1 -1
|
@@ -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
|
|
44
|
-
|
|
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,
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
165
|
+
// High: API keys, generic tokens
|
|
163
166
|
if (lowerID.includes("api-key") ||
|
|
164
|
-
lowerID.includes("
|
|
165
|
-
lowerID.
|
|
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
|
|
10
|
-
* Returns
|
|
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
|
|
12
|
+
export function handle403(e) {
|
|
13
13
|
if (!e || e.response?.status !== 403)
|
|
14
|
-
return
|
|
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
|
|
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,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.
|
|
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.
|
|
4
|
+
version: 0.6.4
|
|
5
5
|
disable-model-invocation: true
|
|
6
6
|
allowed-tools: [Bash, Read, Glob, Grep]
|
|
7
7
|
---
|