@rafter-security/cli 0.4.1 → 0.5.1
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 +100 -0
- package/dist/commands/agent/audit.js +15 -3
- package/dist/commands/agent/exec.js +9 -8
- package/dist/commands/agent/init.js +62 -26
- package/dist/commands/agent/scan.js +113 -101
- package/dist/commands/ci/index.js +8 -0
- package/dist/commands/ci/init.js +191 -0
- package/dist/commands/hook/index.js +8 -0
- package/dist/commands/hook/pretool.js +122 -0
- package/dist/commands/mcp/index.js +8 -0
- package/dist/commands/mcp/server.js +205 -0
- package/dist/commands/policy/export.js +81 -0
- package/dist/commands/policy/index.js +8 -0
- package/dist/core/audit-logger.js +2 -33
- package/dist/core/command-interceptor.js +6 -50
- package/dist/core/config-defaults.js +4 -15
- package/dist/core/config-manager.js +52 -0
- package/dist/core/policy-loader.js +167 -0
- package/dist/core/risk-rules.js +67 -0
- package/dist/index.js +23 -2
- package/dist/scanners/gitleaks.js +7 -6
- package/dist/scanners/regex-scanner.js +22 -2
- package/dist/utils/formatter.js +52 -0
- package/package.json +7 -3
- package/resources/pre-commit-hook.sh +45 -0
- package/resources/rafter-security-skill.md +316 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
import yaml from "js-yaml";
|
|
5
|
+
const POLICY_FILENAMES = [".rafter.yml", ".rafter.yaml"];
|
|
6
|
+
/**
|
|
7
|
+
* Find a policy file by walking from cwd up to git root
|
|
8
|
+
*/
|
|
9
|
+
export function findPolicyFile() {
|
|
10
|
+
let dir = process.cwd();
|
|
11
|
+
const root = getGitRoot() || path.parse(dir).root;
|
|
12
|
+
while (true) {
|
|
13
|
+
for (const filename of POLICY_FILENAMES) {
|
|
14
|
+
const candidate = path.join(dir, filename);
|
|
15
|
+
if (fs.existsSync(candidate)) {
|
|
16
|
+
return candidate;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const parent = path.dirname(dir);
|
|
20
|
+
if (parent === dir || dir === root) {
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
dir = parent;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Load and parse the policy file, returning null if not found
|
|
29
|
+
*/
|
|
30
|
+
export function loadPolicy() {
|
|
31
|
+
const policyPath = findPolicyFile();
|
|
32
|
+
if (!policyPath)
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
const content = fs.readFileSync(policyPath, "utf-8");
|
|
36
|
+
const parsed = yaml.load(content);
|
|
37
|
+
if (!parsed || typeof parsed !== "object")
|
|
38
|
+
return null;
|
|
39
|
+
return validatePolicy(mapPolicy(parsed), parsed);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
console.error(`Warning: Failed to parse policy file ${policyPath}: ${e.message}`);
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Map snake_case YAML keys to camelCase PolicyFile
|
|
48
|
+
*/
|
|
49
|
+
function mapPolicy(raw) {
|
|
50
|
+
const policy = {};
|
|
51
|
+
if (raw.version)
|
|
52
|
+
policy.version = String(raw.version);
|
|
53
|
+
if (raw.risk_level)
|
|
54
|
+
policy.riskLevel = raw.risk_level;
|
|
55
|
+
if (raw.command_policy && typeof raw.command_policy === "object") {
|
|
56
|
+
policy.commandPolicy = {};
|
|
57
|
+
if (raw.command_policy.mode)
|
|
58
|
+
policy.commandPolicy.mode = raw.command_policy.mode;
|
|
59
|
+
if (Array.isArray(raw.command_policy.blocked_patterns)) {
|
|
60
|
+
policy.commandPolicy.blockedPatterns = raw.command_policy.blocked_patterns;
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(raw.command_policy.require_approval)) {
|
|
63
|
+
policy.commandPolicy.requireApproval = raw.command_policy.require_approval;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (raw.scan && typeof raw.scan === "object") {
|
|
67
|
+
policy.scan = {};
|
|
68
|
+
if (Array.isArray(raw.scan.exclude_paths)) {
|
|
69
|
+
policy.scan.excludePaths = raw.scan.exclude_paths;
|
|
70
|
+
}
|
|
71
|
+
if (Array.isArray(raw.scan.custom_patterns)) {
|
|
72
|
+
policy.scan.customPatterns = raw.scan.custom_patterns.map((p) => ({
|
|
73
|
+
name: p.name,
|
|
74
|
+
regex: p.regex,
|
|
75
|
+
severity: p.severity || "high",
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (raw.audit && typeof raw.audit === "object") {
|
|
80
|
+
policy.audit = {};
|
|
81
|
+
if (raw.audit.retention_days != null) {
|
|
82
|
+
policy.audit.retentionDays = Number(raw.audit.retention_days);
|
|
83
|
+
}
|
|
84
|
+
if (raw.audit.log_level)
|
|
85
|
+
policy.audit.logLevel = raw.audit.log_level;
|
|
86
|
+
}
|
|
87
|
+
return policy;
|
|
88
|
+
}
|
|
89
|
+
const VALID_TOP_LEVEL_KEYS = new Set(["version", "risk_level", "command_policy", "scan", "audit"]);
|
|
90
|
+
const VALID_RISK_LEVELS = new Set(["minimal", "moderate", "aggressive"]);
|
|
91
|
+
const VALID_COMMAND_MODES = new Set(["allow-all", "approve-dangerous", "deny-list"]);
|
|
92
|
+
const VALID_LOG_LEVELS = new Set(["debug", "info", "warn", "error"]);
|
|
93
|
+
/**
|
|
94
|
+
* Validate a mapped policy, warn on stderr for invalid fields, strip them out.
|
|
95
|
+
* `raw` is the original parsed YAML (snake_case keys) for unknown-key detection.
|
|
96
|
+
*/
|
|
97
|
+
function validatePolicy(policy, raw) {
|
|
98
|
+
// 1. Unknown top-level keys
|
|
99
|
+
for (const key of Object.keys(raw)) {
|
|
100
|
+
if (!VALID_TOP_LEVEL_KEYS.has(key)) {
|
|
101
|
+
console.error(`Warning: Unknown policy key "${key}" — ignoring.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// 2. Type checking + strip invalid
|
|
105
|
+
if (policy.version !== undefined && typeof policy.version !== "string") {
|
|
106
|
+
console.error(`Warning: "version" must be a string — ignoring.`);
|
|
107
|
+
delete policy.version;
|
|
108
|
+
}
|
|
109
|
+
if (policy.riskLevel !== undefined && !VALID_RISK_LEVELS.has(policy.riskLevel)) {
|
|
110
|
+
console.error(`Warning: "risk_level" must be one of: minimal, moderate, aggressive — ignoring.`);
|
|
111
|
+
delete policy.riskLevel;
|
|
112
|
+
}
|
|
113
|
+
if (policy.commandPolicy) {
|
|
114
|
+
if (policy.commandPolicy.mode !== undefined && !VALID_COMMAND_MODES.has(policy.commandPolicy.mode)) {
|
|
115
|
+
console.error(`Warning: "command_policy.mode" must be one of: allow-all, approve-dangerous, deny-list — ignoring.`);
|
|
116
|
+
delete policy.commandPolicy.mode;
|
|
117
|
+
}
|
|
118
|
+
if (policy.commandPolicy.blockedPatterns !== undefined) {
|
|
119
|
+
if (!Array.isArray(policy.commandPolicy.blockedPatterns) || !policy.commandPolicy.blockedPatterns.every((v) => typeof v === "string")) {
|
|
120
|
+
console.error(`Warning: "command_policy.blocked_patterns" must be an array of strings — ignoring.`);
|
|
121
|
+
delete policy.commandPolicy.blockedPatterns;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (policy.commandPolicy.requireApproval !== undefined) {
|
|
125
|
+
if (!Array.isArray(policy.commandPolicy.requireApproval) || !policy.commandPolicy.requireApproval.every((v) => typeof v === "string")) {
|
|
126
|
+
console.error(`Warning: "command_policy.require_approval" must be an array of strings — ignoring.`);
|
|
127
|
+
delete policy.commandPolicy.requireApproval;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (policy.scan) {
|
|
132
|
+
if (policy.scan.excludePaths !== undefined) {
|
|
133
|
+
if (!Array.isArray(policy.scan.excludePaths) || !policy.scan.excludePaths.every((v) => typeof v === "string")) {
|
|
134
|
+
console.error(`Warning: "scan.exclude_paths" must be an array of strings — ignoring.`);
|
|
135
|
+
delete policy.scan.excludePaths;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (policy.scan.customPatterns !== undefined) {
|
|
139
|
+
if (!Array.isArray(policy.scan.customPatterns) || !policy.scan.customPatterns.every((v) => v && typeof v === "object" && typeof v.name === "string" && v.name !== "" && typeof v.regex === "string" && v.regex !== "" && typeof v.severity === "string")) {
|
|
140
|
+
console.error(`Warning: "scan.custom_patterns" must be an array of objects with name, regex, severity — ignoring.`);
|
|
141
|
+
delete policy.scan.customPatterns;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (policy.audit) {
|
|
146
|
+
if (policy.audit.retentionDays !== undefined && (typeof policy.audit.retentionDays !== "number" || isNaN(policy.audit.retentionDays))) {
|
|
147
|
+
console.error(`Warning: "audit.retention_days" must be a number — ignoring.`);
|
|
148
|
+
delete policy.audit.retentionDays;
|
|
149
|
+
}
|
|
150
|
+
if (policy.audit.logLevel !== undefined && !VALID_LOG_LEVELS.has(policy.audit.logLevel)) {
|
|
151
|
+
console.error(`Warning: "audit.log_level" must be one of: debug, info, warn, error — ignoring.`);
|
|
152
|
+
delete policy.audit.logLevel;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return policy;
|
|
156
|
+
}
|
|
157
|
+
function getGitRoot() {
|
|
158
|
+
try {
|
|
159
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
160
|
+
encoding: "utf-8",
|
|
161
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
162
|
+
}).trim();
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized risk assessment rules.
|
|
3
|
+
* Single source of truth — imported by command-interceptor, audit-logger, and config-defaults.
|
|
4
|
+
*/
|
|
5
|
+
export const CRITICAL_PATTERNS = [
|
|
6
|
+
/rm\s+-rf\s+\//,
|
|
7
|
+
/:\(\)\{\s*:\|:&\s*\};:/, // fork bomb
|
|
8
|
+
/dd\s+if=.*of=\/dev\/sd/,
|
|
9
|
+
/>\s*\/dev\/sd/,
|
|
10
|
+
/mkfs/,
|
|
11
|
+
/fdisk/,
|
|
12
|
+
/parted/,
|
|
13
|
+
];
|
|
14
|
+
export const HIGH_PATTERNS = [
|
|
15
|
+
/rm\s+-rf/,
|
|
16
|
+
/sudo\s+rm/,
|
|
17
|
+
/chmod\s+777/,
|
|
18
|
+
/curl.*\|\s*(bash|sh|zsh|dash)\b/,
|
|
19
|
+
/wget.*\|\s*(bash|sh|zsh|dash)\b/,
|
|
20
|
+
/git\s+push\s+--force/,
|
|
21
|
+
/docker\s+system\s+prune/,
|
|
22
|
+
/npm\s+publish/,
|
|
23
|
+
/pypi.*upload/,
|
|
24
|
+
];
|
|
25
|
+
export const MEDIUM_PATTERNS = [
|
|
26
|
+
/sudo/,
|
|
27
|
+
/chmod/,
|
|
28
|
+
/chown/,
|
|
29
|
+
/systemctl/,
|
|
30
|
+
/service/,
|
|
31
|
+
/kill\s+-9/,
|
|
32
|
+
/pkill/,
|
|
33
|
+
/killall/,
|
|
34
|
+
];
|
|
35
|
+
export const DEFAULT_BLOCKED_PATTERNS = [
|
|
36
|
+
"rm -rf /",
|
|
37
|
+
":(){ :|:& };:",
|
|
38
|
+
"dd if=/dev/zero of=/dev/sda",
|
|
39
|
+
"> /dev/sda",
|
|
40
|
+
];
|
|
41
|
+
export const DEFAULT_REQUIRE_APPROVAL = [
|
|
42
|
+
"rm -rf",
|
|
43
|
+
"sudo rm",
|
|
44
|
+
"curl.*\\|\\s*(bash|sh|zsh|dash)\\b",
|
|
45
|
+
"wget.*\\|\\s*(bash|sh|zsh|dash)\\b",
|
|
46
|
+
"chmod 777",
|
|
47
|
+
"git push --force",
|
|
48
|
+
];
|
|
49
|
+
/**
|
|
50
|
+
* Assess risk level of a command string.
|
|
51
|
+
*/
|
|
52
|
+
export function assessCommandRisk(command) {
|
|
53
|
+
const cmd = command.toLowerCase();
|
|
54
|
+
for (const pattern of CRITICAL_PATTERNS) {
|
|
55
|
+
if (pattern.test(cmd))
|
|
56
|
+
return "critical";
|
|
57
|
+
}
|
|
58
|
+
for (const pattern of HIGH_PATTERNS) {
|
|
59
|
+
if (pattern.test(cmd))
|
|
60
|
+
return "high";
|
|
61
|
+
}
|
|
62
|
+
for (const pattern of MEDIUM_PATTERNS) {
|
|
63
|
+
if (pattern.test(cmd))
|
|
64
|
+
return "medium";
|
|
65
|
+
}
|
|
66
|
+
return "low";
|
|
67
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5,19 +5,40 @@ import { createRunCommand } from "./commands/backend/run.js";
|
|
|
5
5
|
import { createGetCommand } from "./commands/backend/get.js";
|
|
6
6
|
import { createUsageCommand } from "./commands/backend/usage.js";
|
|
7
7
|
import { createAgentCommand } from "./commands/agent/index.js";
|
|
8
|
+
import { createCiCommand } from "./commands/ci/index.js";
|
|
9
|
+
import { createHookCommand } from "./commands/hook/index.js";
|
|
10
|
+
import { createMcpCommand } from "./commands/mcp/index.js";
|
|
11
|
+
import { createPolicyCommand } from "./commands/policy/index.js";
|
|
8
12
|
import { checkForUpdate } from "./utils/update-checker.js";
|
|
13
|
+
import { setAgentMode } from "./utils/formatter.js";
|
|
9
14
|
dotenv.config();
|
|
10
|
-
const VERSION = "0.
|
|
15
|
+
const VERSION = "0.5.0";
|
|
11
16
|
const program = new Command()
|
|
12
17
|
.name("rafter")
|
|
13
18
|
.description("Rafter CLI")
|
|
14
|
-
.version(VERSION)
|
|
19
|
+
.version(VERSION)
|
|
20
|
+
.option("-a, --agent", "Plain output for AI agents (no colors/emoji)");
|
|
21
|
+
// Set agent mode before any subcommand runs
|
|
22
|
+
program.hook("preAction", (thisCommand) => {
|
|
23
|
+
const opts = thisCommand.opts();
|
|
24
|
+
if (opts.agent) {
|
|
25
|
+
setAgentMode(true);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
15
28
|
// Backend commands (existing)
|
|
16
29
|
program.addCommand(createRunCommand());
|
|
17
30
|
program.addCommand(createGetCommand());
|
|
18
31
|
program.addCommand(createUsageCommand());
|
|
19
32
|
// Agent commands
|
|
20
33
|
program.addCommand(createAgentCommand());
|
|
34
|
+
// CI commands
|
|
35
|
+
program.addCommand(createCiCommand());
|
|
36
|
+
// Hook commands (for agent platform integration)
|
|
37
|
+
program.addCommand(createHookCommand());
|
|
38
|
+
// MCP server
|
|
39
|
+
program.addCommand(createMcpCommand());
|
|
40
|
+
// Policy commands
|
|
41
|
+
program.addCommand(createPolicyCommand());
|
|
21
42
|
// Non-blocking update check — runs after command, prints to stderr
|
|
22
43
|
checkForUpdate(VERSION).then((notice) => {
|
|
23
44
|
if (notice)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { promisify } from "util";
|
|
3
3
|
import { BinaryManager } from "../utils/binary-manager.js";
|
|
4
4
|
import fs from "fs";
|
|
5
|
+
import os from "os";
|
|
5
6
|
import path from "path";
|
|
6
|
-
const
|
|
7
|
+
const execFileAsync = promisify(execFile);
|
|
7
8
|
export class GitleaksScanner {
|
|
8
9
|
constructor() {
|
|
9
10
|
this.binaryManager = new BinaryManager();
|
|
@@ -25,10 +26,10 @@ export class GitleaksScanner {
|
|
|
25
26
|
throw new Error("Gitleaks not available");
|
|
26
27
|
}
|
|
27
28
|
const gitleaksPath = this.binaryManager.getGitleaksPath();
|
|
28
|
-
const tmpReport = path.join(
|
|
29
|
+
const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
29
30
|
try {
|
|
30
31
|
// Run gitleaks detect on file
|
|
31
|
-
await
|
|
32
|
+
await execFileAsync(gitleaksPath, ["detect", "--no-git", "-f", "json", "-r", tmpReport, "-s", filePath], { timeout: 30000 });
|
|
32
33
|
// If no leaks found, gitleaks exits 0 with empty report
|
|
33
34
|
if (!fs.existsSync(tmpReport)) {
|
|
34
35
|
return { file: filePath, matches: [] };
|
|
@@ -85,10 +86,10 @@ export class GitleaksScanner {
|
|
|
85
86
|
throw new Error("Gitleaks not available");
|
|
86
87
|
}
|
|
87
88
|
const gitleaksPath = this.binaryManager.getGitleaksPath();
|
|
88
|
-
const tmpReport = path.join(
|
|
89
|
+
const tmpReport = path.join(os.tmpdir(), `gitleaks-${Date.now()}-${Math.random().toString(36).slice(2, 8)}.json`);
|
|
89
90
|
try {
|
|
90
91
|
// Run gitleaks detect on directory
|
|
91
|
-
await
|
|
92
|
+
await execFileAsync(gitleaksPath, ["detect", "--no-git", "-f", "json", "-r", tmpReport, "-s", dirPath], { timeout: 60000 });
|
|
92
93
|
// No leaks found
|
|
93
94
|
if (!fs.existsSync(tmpReport)) {
|
|
94
95
|
return [];
|
|
@@ -3,8 +3,18 @@ import path from "path";
|
|
|
3
3
|
import { PatternEngine } from "../core/pattern-engine.js";
|
|
4
4
|
import { DEFAULT_SECRET_PATTERNS } from "./secret-patterns.js";
|
|
5
5
|
export class RegexScanner {
|
|
6
|
-
constructor() {
|
|
7
|
-
|
|
6
|
+
constructor(customPatterns) {
|
|
7
|
+
const patterns = [...DEFAULT_SECRET_PATTERNS];
|
|
8
|
+
if (customPatterns) {
|
|
9
|
+
for (const cp of customPatterns) {
|
|
10
|
+
patterns.push({
|
|
11
|
+
name: cp.name,
|
|
12
|
+
regex: cp.regex,
|
|
13
|
+
severity: cp.severity,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
this.engine = new PatternEngine(patterns);
|
|
8
18
|
}
|
|
9
19
|
/**
|
|
10
20
|
* Scan a single file for secrets
|
|
@@ -53,6 +63,16 @@ export class RegexScanner {
|
|
|
53
63
|
".vscode",
|
|
54
64
|
".idea"
|
|
55
65
|
];
|
|
66
|
+
// Merge policy excludePaths into the exclude list
|
|
67
|
+
if (options?.excludePaths) {
|
|
68
|
+
for (const ep of options.excludePaths) {
|
|
69
|
+
// Strip trailing slashes for directory name matching
|
|
70
|
+
const cleaned = ep.replace(/\/+$/, "");
|
|
71
|
+
if (!exclude.includes(cleaned)) {
|
|
72
|
+
exclude.push(cleaned);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
56
76
|
const files = this.walkDirectory(dirPath, exclude, options?.maxDepth || 10);
|
|
57
77
|
return this.scanFiles(files);
|
|
58
78
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
let agentMode = false;
|
|
3
|
+
export function setAgentMode(enabled) {
|
|
4
|
+
agentMode = enabled;
|
|
5
|
+
}
|
|
6
|
+
export function isAgentMode() {
|
|
7
|
+
return agentMode;
|
|
8
|
+
}
|
|
9
|
+
export const fmt = {
|
|
10
|
+
header(text) {
|
|
11
|
+
if (agentMode)
|
|
12
|
+
return `=== ${text} ===`;
|
|
13
|
+
return chalk.bold(`\n${chalk.cyan("┌─")} ${text} ${chalk.cyan("─┐")}\n`);
|
|
14
|
+
},
|
|
15
|
+
success(text) {
|
|
16
|
+
if (agentMode)
|
|
17
|
+
return `[OK] ${text}`;
|
|
18
|
+
return chalk.green(`✓ ${text}`);
|
|
19
|
+
},
|
|
20
|
+
warning(text) {
|
|
21
|
+
if (agentMode)
|
|
22
|
+
return `[WARN] ${text}`;
|
|
23
|
+
return chalk.yellow(`⚠️ ${text}`);
|
|
24
|
+
},
|
|
25
|
+
error(text) {
|
|
26
|
+
if (agentMode)
|
|
27
|
+
return `[ERROR] ${text}`;
|
|
28
|
+
return chalk.red(`✗ ${text}`);
|
|
29
|
+
},
|
|
30
|
+
severity(level) {
|
|
31
|
+
const upper = level.toUpperCase();
|
|
32
|
+
if (agentMode)
|
|
33
|
+
return `[${upper}]`;
|
|
34
|
+
switch (level) {
|
|
35
|
+
case "critical": return chalk.bgRed.white.bold(` ${upper} `);
|
|
36
|
+
case "high": return chalk.bgYellow.black.bold(` ${upper} `);
|
|
37
|
+
case "medium": return chalk.bgBlue.white(` ${upper} `);
|
|
38
|
+
case "low": return chalk.bgGreen.white(` ${upper} `);
|
|
39
|
+
default: return `[${upper}]`;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
divider() {
|
|
43
|
+
if (agentMode)
|
|
44
|
+
return "---";
|
|
45
|
+
return chalk.gray("═".repeat(50));
|
|
46
|
+
},
|
|
47
|
+
info(text) {
|
|
48
|
+
if (agentMode)
|
|
49
|
+
return text;
|
|
50
|
+
return chalk.cyan(text);
|
|
51
|
+
},
|
|
52
|
+
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rafter-security/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"rafter": "./dist/index.js"
|
|
7
7
|
},
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"resources"
|
|
10
11
|
],
|
|
11
12
|
"scripts": {
|
|
12
13
|
"build": "tsc -p tsconfig.json",
|
|
@@ -18,17 +19,20 @@
|
|
|
18
19
|
},
|
|
19
20
|
"license": "MIT",
|
|
20
21
|
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
21
23
|
"axios": "^1.6.8",
|
|
22
24
|
"chalk": "^5.3.0",
|
|
23
25
|
"commander": "^11.1.0",
|
|
24
26
|
"dotenv": "^16.4.5",
|
|
27
|
+
"js-yaml": "^4.1.0",
|
|
25
28
|
"ora": "^7.0.1",
|
|
26
29
|
"tar": "^7.5.7"
|
|
27
30
|
},
|
|
28
31
|
"devDependencies": {
|
|
32
|
+
"@types/js-yaml": "^4.0.9",
|
|
29
33
|
"@types/node": "^20.11.30",
|
|
30
34
|
"tsx": "^4.7.0",
|
|
31
35
|
"typescript": "^5.4.5",
|
|
32
|
-
"vitest": "^
|
|
36
|
+
"vitest": "^4.0.18"
|
|
33
37
|
}
|
|
34
38
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Rafter Security Pre-Commit Hook
|
|
3
|
+
# Scans staged files for secrets before allowing commits
|
|
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
|
+
# Get list of staged files
|
|
20
|
+
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
|
|
21
|
+
|
|
22
|
+
if [ -z "$STAGED_FILES" ]; then
|
|
23
|
+
# No files staged
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
echo "🔍 Rafter: Scanning staged files for secrets..."
|
|
28
|
+
|
|
29
|
+
# Scan staged files
|
|
30
|
+
rafter agent scan --staged --quiet
|
|
31
|
+
|
|
32
|
+
EXIT_CODE=$?
|
|
33
|
+
|
|
34
|
+
if [ $EXIT_CODE -ne 0 ]; then
|
|
35
|
+
echo -e "${RED}❌ Commit blocked: Secrets detected in staged files${NC}"
|
|
36
|
+
echo ""
|
|
37
|
+
echo " Run: rafter agent scan --staged"
|
|
38
|
+
echo " To see details and remediate."
|
|
39
|
+
echo ""
|
|
40
|
+
echo " To bypass (NOT recommended): git commit --no-verify"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo -e "${GREEN}✓ No secrets detected${NC}"
|
|
45
|
+
exit 0
|