@rafter-security/cli 0.4.2 → 0.5.3
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 +101 -1
- package/dist/commands/agent/audit-skill.js +6 -0
- package/dist/commands/agent/audit.js +15 -3
- package/dist/commands/agent/exec.js +9 -8
- package/dist/commands/agent/index.js +4 -0
- package/dist/commands/agent/init.js +132 -47
- package/dist/commands/agent/install-hook.js +2 -1
- package/dist/commands/agent/scan.js +180 -103
- package/dist/commands/agent/status.js +115 -0
- package/dist/commands/agent/verify.js +117 -0
- package/dist/commands/ci/index.js +8 -0
- package/dist/commands/ci/init.js +191 -0
- package/dist/commands/completion.js +170 -0
- package/dist/commands/hook/index.js +10 -0
- package/dist/commands/hook/posttool.js +73 -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 +68 -0
- package/dist/core/custom-patterns.js +157 -0
- package/dist/core/policy-loader.js +167 -0
- package/dist/core/risk-rules.js +72 -0
- package/dist/index.js +26 -2
- package/dist/scanners/gitleaks.js +7 -6
- package/dist/scanners/regex-scanner.js +28 -12
- package/dist/utils/binary-manager.js +100 -7
- package/dist/utils/formatter.js +52 -0
- package/dist/utils/skill-manager.js +22 -9
- package/package.json +7 -3
- package/resources/pre-commit-hook.sh +45 -0
- package/resources/rafter-security-skill.md +323 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load custom secret patterns from ~/.rafter/patterns/
|
|
3
|
+
* and suppression rules from .rafterignore.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { getRafterDir } from "./config-defaults.js";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Custom pattern loading
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
/**
|
|
12
|
+
* Load user-defined patterns from ~/.rafter/patterns/*.txt and *.json.
|
|
13
|
+
*
|
|
14
|
+
* .txt — one regex per line (comments with # ignored)
|
|
15
|
+
* .json — array of {name, pattern, severity?} objects
|
|
16
|
+
*
|
|
17
|
+
* Returns Pattern[] merged with DEFAULT_SECRET_PATTERNS by callers.
|
|
18
|
+
*/
|
|
19
|
+
export function loadCustomPatterns() {
|
|
20
|
+
const patternsDir = path.join(getRafterDir(), "patterns");
|
|
21
|
+
if (!fs.existsSync(patternsDir))
|
|
22
|
+
return [];
|
|
23
|
+
const results = [];
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = fs.readdirSync(patternsDir, { withFileTypes: true });
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (!entry.isFile())
|
|
33
|
+
continue;
|
|
34
|
+
const file = path.join(patternsDir, entry.name);
|
|
35
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
36
|
+
if (ext === ".txt") {
|
|
37
|
+
results.push(...loadTxtPatterns(file));
|
|
38
|
+
}
|
|
39
|
+
else if (ext === ".json") {
|
|
40
|
+
results.push(...loadJsonPatterns(file));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return results;
|
|
44
|
+
}
|
|
45
|
+
function loadTxtPatterns(file) {
|
|
46
|
+
try {
|
|
47
|
+
const lines = fs.readFileSync(file, "utf-8").split("\n");
|
|
48
|
+
const patterns = [];
|
|
49
|
+
for (const raw of lines) {
|
|
50
|
+
const line = raw.trim();
|
|
51
|
+
if (!line || line.startsWith("#"))
|
|
52
|
+
continue;
|
|
53
|
+
patterns.push({
|
|
54
|
+
name: `Custom (${path.basename(file, ".txt")})`,
|
|
55
|
+
regex: line,
|
|
56
|
+
severity: "high",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return patterns;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function loadJsonPatterns(file) {
|
|
66
|
+
try {
|
|
67
|
+
const data = JSON.parse(fs.readFileSync(file, "utf-8"));
|
|
68
|
+
if (!Array.isArray(data))
|
|
69
|
+
return [];
|
|
70
|
+
const patterns = [];
|
|
71
|
+
for (const entry of data) {
|
|
72
|
+
if (typeof entry.pattern !== "string")
|
|
73
|
+
continue;
|
|
74
|
+
patterns.push({
|
|
75
|
+
name: entry.name ?? `Custom (${path.basename(file, ".json")})`,
|
|
76
|
+
regex: entry.pattern,
|
|
77
|
+
severity: entry.severity ?? "high",
|
|
78
|
+
description: entry.description,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return patterns;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse .rafterignore from the given directory (project root).
|
|
89
|
+
*
|
|
90
|
+
* Format — one entry per line:
|
|
91
|
+
* path/glob → suppress all findings in matching files
|
|
92
|
+
* path/glob:pattern-name → suppress specific pattern in matching files
|
|
93
|
+
*
|
|
94
|
+
* Lines starting with # are comments.
|
|
95
|
+
*/
|
|
96
|
+
export function loadSuppressions(projectRoot = process.cwd()) {
|
|
97
|
+
const file = path.join(projectRoot, ".rafterignore");
|
|
98
|
+
if (!fs.existsSync(file))
|
|
99
|
+
return [];
|
|
100
|
+
const suppressions = [];
|
|
101
|
+
try {
|
|
102
|
+
const lines = fs.readFileSync(file, "utf-8").split("\n");
|
|
103
|
+
for (const raw of lines) {
|
|
104
|
+
const line = raw.trim();
|
|
105
|
+
if (!line || line.startsWith("#"))
|
|
106
|
+
continue;
|
|
107
|
+
const colonIdx = line.indexOf(":");
|
|
108
|
+
if (colonIdx === -1) {
|
|
109
|
+
suppressions.push({ pathGlob: line });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
suppressions.push({
|
|
113
|
+
pathGlob: line.slice(0, colonIdx).trim(),
|
|
114
|
+
patternName: line.slice(colonIdx + 1).trim() || undefined,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// ignore unreadable .rafterignore
|
|
121
|
+
}
|
|
122
|
+
return suppressions;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Returns true if a finding should be suppressed.
|
|
126
|
+
*/
|
|
127
|
+
export function isSuppressed(filePath, patternName, suppressions) {
|
|
128
|
+
for (const s of suppressions) {
|
|
129
|
+
if (matchGlob(s.pathGlob, filePath)) {
|
|
130
|
+
if (!s.patternName || s.patternName.toLowerCase() === patternName.toLowerCase()) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Minimal glob matcher: supports * (within segment) and ** (cross-segment).
|
|
139
|
+
* Not full micromatch — covers the 90% case for .rafterignore.
|
|
140
|
+
*/
|
|
141
|
+
function matchGlob(glob, filePath) {
|
|
142
|
+
// Normalise separators
|
|
143
|
+
const g = glob.replace(/\\/g, "/");
|
|
144
|
+
const f = filePath.replace(/\\/g, "/");
|
|
145
|
+
// Escape regex special chars except * which we handle specially
|
|
146
|
+
const escaped = g
|
|
147
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
148
|
+
.replace(/\*\*/g, "\x00") // placeholder for **
|
|
149
|
+
.replace(/\*/g, "[^/]*") // * = anything within one segment
|
|
150
|
+
.replace(/\x00/g, ".*"); // ** = anything including /
|
|
151
|
+
try {
|
|
152
|
+
return new RegExp(`(^|/)${escaped}(/|$)`).test(f);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -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,72 @@
|
|
|
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|-f)\b/,
|
|
21
|
+
/git\s+push\s+--force-(with-lease|if-includes)\b/,
|
|
22
|
+
/git\s+push\s+\S*\s+\+\S+/, // refspec force: git push origin +main
|
|
23
|
+
/docker\s+system\s+prune/,
|
|
24
|
+
/npm\s+publish/,
|
|
25
|
+
/pypi.*upload/,
|
|
26
|
+
];
|
|
27
|
+
export const MEDIUM_PATTERNS = [
|
|
28
|
+
/sudo/,
|
|
29
|
+
/chmod/,
|
|
30
|
+
/chown/,
|
|
31
|
+
/systemctl/,
|
|
32
|
+
/service/,
|
|
33
|
+
/kill\s+-9/,
|
|
34
|
+
/pkill/,
|
|
35
|
+
/killall/,
|
|
36
|
+
];
|
|
37
|
+
export const DEFAULT_BLOCKED_PATTERNS = [
|
|
38
|
+
"rm -rf /",
|
|
39
|
+
":(){ :|:& };:",
|
|
40
|
+
"dd if=/dev/zero of=/dev/sda",
|
|
41
|
+
"> /dev/sda",
|
|
42
|
+
];
|
|
43
|
+
export const DEFAULT_REQUIRE_APPROVAL = [
|
|
44
|
+
"rm -rf",
|
|
45
|
+
"sudo rm",
|
|
46
|
+
"curl.*\\|\\s*(bash|sh|zsh|dash)\\b",
|
|
47
|
+
"wget.*\\|\\s*(bash|sh|zsh|dash)\\b",
|
|
48
|
+
"chmod 777",
|
|
49
|
+
"git push --force",
|
|
50
|
+
"git push -f",
|
|
51
|
+
"git push --force-with-lease",
|
|
52
|
+
"git push --force-if-includes",
|
|
53
|
+
];
|
|
54
|
+
/**
|
|
55
|
+
* Assess risk level of a command string.
|
|
56
|
+
*/
|
|
57
|
+
export function assessCommandRisk(command) {
|
|
58
|
+
const cmd = command.toLowerCase();
|
|
59
|
+
for (const pattern of CRITICAL_PATTERNS) {
|
|
60
|
+
if (pattern.test(cmd))
|
|
61
|
+
return "critical";
|
|
62
|
+
}
|
|
63
|
+
for (const pattern of HIGH_PATTERNS) {
|
|
64
|
+
if (pattern.test(cmd))
|
|
65
|
+
return "high";
|
|
66
|
+
}
|
|
67
|
+
for (const pattern of MEDIUM_PATTERNS) {
|
|
68
|
+
if (pattern.test(cmd))
|
|
69
|
+
return "medium";
|
|
70
|
+
}
|
|
71
|
+
return "low";
|
|
72
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -5,19 +5,43 @@ 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";
|
|
12
|
+
import { createCompletionCommand } from "./commands/completion.js";
|
|
8
13
|
import { checkForUpdate } from "./utils/update-checker.js";
|
|
14
|
+
import { setAgentMode } from "./utils/formatter.js";
|
|
9
15
|
dotenv.config();
|
|
10
|
-
const VERSION = "0.
|
|
16
|
+
const VERSION = "0.5.3";
|
|
11
17
|
const program = new Command()
|
|
12
18
|
.name("rafter")
|
|
13
19
|
.description("Rafter CLI")
|
|
14
|
-
.version(VERSION)
|
|
20
|
+
.version(VERSION)
|
|
21
|
+
.option("-a, --agent", "Plain output for AI agents (no colors/emoji)");
|
|
22
|
+
// Set agent mode before any subcommand runs
|
|
23
|
+
program.hook("preAction", (thisCommand) => {
|
|
24
|
+
const opts = thisCommand.opts();
|
|
25
|
+
if (opts.agent) {
|
|
26
|
+
setAgentMode(true);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
15
29
|
// Backend commands (existing)
|
|
16
30
|
program.addCommand(createRunCommand());
|
|
17
31
|
program.addCommand(createGetCommand());
|
|
18
32
|
program.addCommand(createUsageCommand());
|
|
19
33
|
// Agent commands
|
|
20
34
|
program.addCommand(createAgentCommand());
|
|
35
|
+
// CI commands
|
|
36
|
+
program.addCommand(createCiCommand());
|
|
37
|
+
// Hook commands (for agent platform integration)
|
|
38
|
+
program.addCommand(createHookCommand());
|
|
39
|
+
// MCP server
|
|
40
|
+
program.addCommand(createMcpCommand());
|
|
41
|
+
// Policy commands
|
|
42
|
+
program.addCommand(createPolicyCommand());
|
|
43
|
+
// Shell completions
|
|
44
|
+
program.addCommand(createCompletionCommand());
|
|
21
45
|
// Non-blocking update check — runs after command, prints to stderr
|
|
22
46
|
checkForUpdate(VERSION).then((notice) => {
|
|
23
47
|
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 [];
|
|
@@ -2,9 +2,21 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { PatternEngine } from "../core/pattern-engine.js";
|
|
4
4
|
import { DEFAULT_SECRET_PATTERNS } from "./secret-patterns.js";
|
|
5
|
+
import { loadCustomPatterns, loadSuppressions, isSuppressed } from "../core/custom-patterns.js";
|
|
5
6
|
export class RegexScanner {
|
|
6
|
-
constructor() {
|
|
7
|
-
|
|
7
|
+
constructor(customPatterns) {
|
|
8
|
+
const patterns = [...DEFAULT_SECRET_PATTERNS, ...loadCustomPatterns()];
|
|
9
|
+
if (customPatterns) {
|
|
10
|
+
for (const cp of customPatterns) {
|
|
11
|
+
patterns.push({
|
|
12
|
+
name: cp.name,
|
|
13
|
+
regex: cp.regex,
|
|
14
|
+
severity: cp.severity,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
this.engine = new PatternEngine(patterns);
|
|
19
|
+
this.suppressions = loadSuppressions();
|
|
8
20
|
}
|
|
9
21
|
/**
|
|
10
22
|
* Scan a single file for secrets
|
|
@@ -12,18 +24,12 @@ export class RegexScanner {
|
|
|
12
24
|
scanFile(filePath) {
|
|
13
25
|
try {
|
|
14
26
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
matches
|
|
19
|
-
};
|
|
27
|
+
const raw = this.engine.scanWithPosition(content);
|
|
28
|
+
const matches = raw.filter((m) => !isSuppressed(filePath, m.pattern.name, this.suppressions));
|
|
29
|
+
return { file: filePath, matches };
|
|
20
30
|
}
|
|
21
31
|
catch (e) {
|
|
22
|
-
|
|
23
|
-
return {
|
|
24
|
-
file: filePath,
|
|
25
|
-
matches: []
|
|
26
|
-
};
|
|
32
|
+
return { file: filePath, matches: [] };
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
35
|
/**
|
|
@@ -53,6 +59,16 @@ export class RegexScanner {
|
|
|
53
59
|
".vscode",
|
|
54
60
|
".idea"
|
|
55
61
|
];
|
|
62
|
+
// Merge policy excludePaths into the exclude list
|
|
63
|
+
if (options?.excludePaths) {
|
|
64
|
+
for (const ep of options.excludePaths) {
|
|
65
|
+
// Strip trailing slashes for directory name matching
|
|
66
|
+
const cleaned = ep.replace(/\/+$/, "");
|
|
67
|
+
if (!exclude.includes(cleaned)) {
|
|
68
|
+
exclude.push(cleaned);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
56
72
|
const files = this.walkDirectory(dirPath, exclude, options?.maxDepth || 10);
|
|
57
73
|
return this.scanFiles(files);
|
|
58
74
|
}
|