@rafter-security/cli 0.4.2 → 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.
@@ -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.4.2";
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 { exec } from "child_process";
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 execAsync = promisify(exec);
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("/tmp", `gitleaks-${Date.now()}.json`);
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 execAsync(`"${gitleaksPath}" detect --no-git -f json -r "${tmpReport}" -s "${filePath}"`, { timeout: 30000 });
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("/tmp", `gitleaks-${Date.now()}.json`);
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 execAsync(`"${gitleaksPath}" detect --no-git -f json -r "${tmpReport}" -s "${dirPath}"`, { timeout: 60000 });
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
- this.engine = new PatternEngine(DEFAULT_SECRET_PATTERNS);
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.4.2",
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": "^1.5.0"
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