@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,191 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fmt } from "../../utils/formatter.js";
|
|
5
|
+
export function createCiInitCommand() {
|
|
6
|
+
return new Command("init")
|
|
7
|
+
.description("Generate CI/CD pipeline config for secret scanning")
|
|
8
|
+
.option("--platform <platform>", "CI platform: github, gitlab, circleci (default: auto-detect)")
|
|
9
|
+
.option("--output <path>", "Output file path (default: platform-specific)")
|
|
10
|
+
.option("--with-backend", "Include backend security audit job (requires RAFTER_API_KEY)")
|
|
11
|
+
.action((opts) => {
|
|
12
|
+
const platform = opts.platform || detectPlatform();
|
|
13
|
+
if (!platform) {
|
|
14
|
+
console.error(fmt.error("Could not auto-detect CI platform."));
|
|
15
|
+
console.error("Specify one with --platform github|gitlab|circleci");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const validPlatforms = ["github", "gitlab", "circleci"];
|
|
19
|
+
if (!validPlatforms.includes(platform)) {
|
|
20
|
+
console.error(fmt.error(`Unknown platform: ${platform}`));
|
|
21
|
+
console.error(`Valid options: ${validPlatforms.join(", ")}`);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const { content, defaultPath } = generateTemplate(platform, !!opts.withBackend);
|
|
25
|
+
const outputPath = opts.output || defaultPath;
|
|
26
|
+
const outputDir = path.dirname(outputPath);
|
|
27
|
+
if (!fs.existsSync(outputDir)) {
|
|
28
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
29
|
+
}
|
|
30
|
+
fs.writeFileSync(outputPath, content, "utf-8");
|
|
31
|
+
console.log(fmt.success(`Generated ${platform} CI config at ${outputPath}`));
|
|
32
|
+
console.log();
|
|
33
|
+
console.log("Next steps:");
|
|
34
|
+
console.log(` 1. Review the generated file: ${outputPath}`);
|
|
35
|
+
if (opts.withBackend) {
|
|
36
|
+
if (platform === "github") {
|
|
37
|
+
console.log(" 2. Add RAFTER_API_KEY to repo Settings > Secrets > Actions");
|
|
38
|
+
}
|
|
39
|
+
else if (platform === "gitlab") {
|
|
40
|
+
console.log(" 2. Add RAFTER_API_KEY to Settings > CI/CD > Variables");
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
console.log(" 2. Add RAFTER_API_KEY to project environment variables");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
console.log(` ${opts.withBackend ? "3" : "2"}. Commit and push to trigger the pipeline`);
|
|
47
|
+
console.log();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function detectPlatform() {
|
|
51
|
+
if (fs.existsSync(".github"))
|
|
52
|
+
return "github";
|
|
53
|
+
if (fs.existsSync(".gitlab-ci.yml"))
|
|
54
|
+
return "gitlab";
|
|
55
|
+
if (fs.existsSync(".circleci"))
|
|
56
|
+
return "circleci";
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
function generateTemplate(platform, withBackend) {
|
|
60
|
+
switch (platform) {
|
|
61
|
+
case "github":
|
|
62
|
+
return { content: githubTemplate(withBackend), defaultPath: ".github/workflows/rafter-security.yml" };
|
|
63
|
+
case "gitlab":
|
|
64
|
+
return { content: gitlabTemplate(withBackend), defaultPath: ".gitlab-ci-rafter.yml" };
|
|
65
|
+
case "circleci":
|
|
66
|
+
return { content: circleciTemplate(withBackend), defaultPath: ".circleci/rafter-security.yml" };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function githubTemplate(withBackend) {
|
|
70
|
+
let yaml = `# Generated by: rafter ci init
|
|
71
|
+
name: Rafter Security
|
|
72
|
+
|
|
73
|
+
on:
|
|
74
|
+
push:
|
|
75
|
+
branches: [main]
|
|
76
|
+
pull_request:
|
|
77
|
+
branches: [main]
|
|
78
|
+
|
|
79
|
+
permissions:
|
|
80
|
+
contents: read
|
|
81
|
+
|
|
82
|
+
jobs:
|
|
83
|
+
secret-scan:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
|
|
88
|
+
- name: Install Rafter CLI
|
|
89
|
+
run: npm install -g @rafter-security/cli
|
|
90
|
+
|
|
91
|
+
- name: Scan for secrets
|
|
92
|
+
run: rafter agent scan . --quiet
|
|
93
|
+
`;
|
|
94
|
+
if (withBackend) {
|
|
95
|
+
yaml += `
|
|
96
|
+
security-audit:
|
|
97
|
+
runs-on: ubuntu-latest
|
|
98
|
+
needs: secret-scan
|
|
99
|
+
steps:
|
|
100
|
+
- uses: actions/checkout@v4
|
|
101
|
+
|
|
102
|
+
- name: Install Rafter CLI
|
|
103
|
+
run: npm install -g @rafter-security/cli
|
|
104
|
+
|
|
105
|
+
- name: Run security audit
|
|
106
|
+
env:
|
|
107
|
+
RAFTER_API_KEY: \${{ secrets.RAFTER_API_KEY }}
|
|
108
|
+
run: rafter run --format json --quiet
|
|
109
|
+
`;
|
|
110
|
+
}
|
|
111
|
+
return yaml;
|
|
112
|
+
}
|
|
113
|
+
function gitlabTemplate(withBackend) {
|
|
114
|
+
let yaml = `# Generated by: rafter ci init
|
|
115
|
+
stages:
|
|
116
|
+
- security
|
|
117
|
+
|
|
118
|
+
secret-scan:
|
|
119
|
+
stage: security
|
|
120
|
+
image: node:20
|
|
121
|
+
script:
|
|
122
|
+
- npm install -g @rafter-security/cli
|
|
123
|
+
- rafter agent scan . --quiet
|
|
124
|
+
rules:
|
|
125
|
+
- if: $CI_PIPELINE_SOURCE == "push"
|
|
126
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
127
|
+
`;
|
|
128
|
+
if (withBackend) {
|
|
129
|
+
yaml += `
|
|
130
|
+
security-audit:
|
|
131
|
+
stage: security
|
|
132
|
+
image: node:20
|
|
133
|
+
needs: [secret-scan]
|
|
134
|
+
script:
|
|
135
|
+
- npm install -g @rafter-security/cli
|
|
136
|
+
- rafter run --format json --quiet
|
|
137
|
+
variables:
|
|
138
|
+
RAFTER_API_KEY: $RAFTER_API_KEY
|
|
139
|
+
rules:
|
|
140
|
+
- if: $CI_PIPELINE_SOURCE == "push"
|
|
141
|
+
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
return yaml;
|
|
145
|
+
}
|
|
146
|
+
function circleciTemplate(withBackend) {
|
|
147
|
+
let yaml = `# Generated by: rafter ci init
|
|
148
|
+
version: 2.1
|
|
149
|
+
|
|
150
|
+
jobs:
|
|
151
|
+
secret-scan:
|
|
152
|
+
docker:
|
|
153
|
+
- image: cimg/node:20.0
|
|
154
|
+
steps:
|
|
155
|
+
- checkout
|
|
156
|
+
- run:
|
|
157
|
+
name: Install Rafter CLI
|
|
158
|
+
command: npm install -g @rafter-security/cli
|
|
159
|
+
- run:
|
|
160
|
+
name: Scan for secrets
|
|
161
|
+
command: rafter agent scan . --quiet
|
|
162
|
+
`;
|
|
163
|
+
if (withBackend) {
|
|
164
|
+
yaml += `
|
|
165
|
+
security-audit:
|
|
166
|
+
docker:
|
|
167
|
+
- image: cimg/node:20.0
|
|
168
|
+
steps:
|
|
169
|
+
- checkout
|
|
170
|
+
- run:
|
|
171
|
+
name: Install Rafter CLI
|
|
172
|
+
command: npm install -g @rafter-security/cli
|
|
173
|
+
- run:
|
|
174
|
+
name: Run security audit
|
|
175
|
+
command: rafter run --format json --quiet
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
178
|
+
yaml += `
|
|
179
|
+
workflows:
|
|
180
|
+
security:
|
|
181
|
+
jobs:
|
|
182
|
+
- secret-scan`;
|
|
183
|
+
if (withBackend) {
|
|
184
|
+
yaml += `
|
|
185
|
+
- security-audit:
|
|
186
|
+
requires:
|
|
187
|
+
- secret-scan`;
|
|
188
|
+
}
|
|
189
|
+
yaml += "\n";
|
|
190
|
+
return yaml;
|
|
191
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
const BASH_COMPLETION = `
|
|
3
|
+
# rafter bash completion
|
|
4
|
+
# Add to ~/.bashrc: eval "$(rafter completion bash)"
|
|
5
|
+
_rafter_completion() {
|
|
6
|
+
local cur prev words
|
|
7
|
+
COMPREPLY=()
|
|
8
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
9
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
10
|
+
words="\${COMP_WORDS[*]}"
|
|
11
|
+
|
|
12
|
+
local top_cmds="run scan get usage agent ci hook mcp policy completion --help --version"
|
|
13
|
+
local agent_cmds="init scan exec config audit audit-skill install-hook verify status"
|
|
14
|
+
local ci_cmds="init"
|
|
15
|
+
local hook_cmds="pretool posttool"
|
|
16
|
+
local policy_cmds="export"
|
|
17
|
+
|
|
18
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
19
|
+
COMPREPLY=( \$(compgen -W "\${top_cmds}" -- "\${cur}") )
|
|
20
|
+
return 0
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
case "\${COMP_WORDS[1]}" in
|
|
24
|
+
agent)
|
|
25
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
26
|
+
COMPREPLY=( \$(compgen -W "\${agent_cmds}" -- "\${cur}") )
|
|
27
|
+
fi
|
|
28
|
+
case "\${COMP_WORDS[2]}" in
|
|
29
|
+
scan) COMPREPLY=( \$(compgen -W "--quiet --json --format --staged --diff --engine" -- "\${cur}") ) ;;
|
|
30
|
+
init) COMPREPLY=( \$(compgen -W "--risk-level --skip-gitleaks --skip-openclaw --skip-claude-code --force" -- "\${cur}") ) ;;
|
|
31
|
+
verify) COMPREPLY=() ;;
|
|
32
|
+
status) COMPREPLY=() ;;
|
|
33
|
+
audit-skill) COMPREPLY=( \$(compgen -W "--skip-openclaw --json" -- "\${cur}") ) ;;
|
|
34
|
+
install-hook) COMPREPLY=( \$(compgen -W "--global" -- "\${cur}") ) ;;
|
|
35
|
+
config) COMPREPLY=( \$(compgen -W "show get set" -- "\${cur}") ) ;;
|
|
36
|
+
audit) COMPREPLY=( \$(compgen -W "--last --event --agent --since" -- "\${cur}") ) ;;
|
|
37
|
+
esac
|
|
38
|
+
;;
|
|
39
|
+
hook)
|
|
40
|
+
COMPREPLY=( \$(compgen -W "\${hook_cmds}" -- "\${cur}") )
|
|
41
|
+
;;
|
|
42
|
+
ci)
|
|
43
|
+
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
44
|
+
COMPREPLY=( \$(compgen -W "\${ci_cmds}" -- "\${cur}") )
|
|
45
|
+
fi
|
|
46
|
+
;;
|
|
47
|
+
policy)
|
|
48
|
+
COMPREPLY=( \$(compgen -W "\${policy_cmds}" -- "\${cur}") )
|
|
49
|
+
;;
|
|
50
|
+
run|scan)
|
|
51
|
+
COMPREPLY=( \$(compgen -W "--api-key --format --quiet" -- "\${cur}") )
|
|
52
|
+
;;
|
|
53
|
+
completion)
|
|
54
|
+
COMPREPLY=( \$(compgen -W "bash zsh fish" -- "\${cur}") )
|
|
55
|
+
;;
|
|
56
|
+
esac
|
|
57
|
+
}
|
|
58
|
+
complete -F _rafter_completion rafter
|
|
59
|
+
`;
|
|
60
|
+
const ZSH_COMPLETION = `
|
|
61
|
+
# rafter zsh completion
|
|
62
|
+
# Add to ~/.zshrc: eval "$(rafter completion zsh)"
|
|
63
|
+
#compdef rafter
|
|
64
|
+
|
|
65
|
+
_rafter() {
|
|
66
|
+
local state
|
|
67
|
+
typeset -A opt_args
|
|
68
|
+
|
|
69
|
+
_arguments \\
|
|
70
|
+
'1: :->cmd' \\
|
|
71
|
+
'*: :->args'
|
|
72
|
+
|
|
73
|
+
case \$state in
|
|
74
|
+
cmd)
|
|
75
|
+
_values 'command' \\
|
|
76
|
+
'run[Run a security scan via backend]' \\
|
|
77
|
+
'scan[Alias for run]' \\
|
|
78
|
+
'agent[Agent security features]' \\
|
|
79
|
+
'ci[CI/CD integration]' \\
|
|
80
|
+
'hook[Hook handlers]' \\
|
|
81
|
+
'mcp[MCP server]' \\
|
|
82
|
+
'policy[Policy management]' \\
|
|
83
|
+
'completion[Shell completion scripts]'
|
|
84
|
+
;;
|
|
85
|
+
args)
|
|
86
|
+
case \$words[2] in
|
|
87
|
+
agent)
|
|
88
|
+
_values 'subcommand' \\
|
|
89
|
+
'init[Initialize agent security]' \\
|
|
90
|
+
'scan[Scan for secrets]' \\
|
|
91
|
+
'exec[Execute command with security validation]' \\
|
|
92
|
+
'config[Manage configuration]' \\
|
|
93
|
+
'audit[View audit logs]' \\
|
|
94
|
+
'audit-skill[Audit a skill file]' \\
|
|
95
|
+
'install-hook[Install git pre-commit hook]' \\
|
|
96
|
+
'verify[Health check]' \\
|
|
97
|
+
'status[Status dashboard]'
|
|
98
|
+
;;
|
|
99
|
+
hook)
|
|
100
|
+
_values 'subcommand' 'pretool[PreToolUse handler]' 'posttool[PostToolUse handler]'
|
|
101
|
+
;;
|
|
102
|
+
completion)
|
|
103
|
+
_values 'shell' 'bash' 'zsh' 'fish'
|
|
104
|
+
;;
|
|
105
|
+
esac
|
|
106
|
+
;;
|
|
107
|
+
esac
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_rafter
|
|
111
|
+
`;
|
|
112
|
+
const FISH_COMPLETION = `
|
|
113
|
+
# rafter fish completion
|
|
114
|
+
# Save to ~/.config/fish/completions/rafter.fish
|
|
115
|
+
# Or: rafter completion fish > ~/.config/fish/completions/rafter.fish
|
|
116
|
+
|
|
117
|
+
complete -c rafter -f
|
|
118
|
+
complete -c rafter -n '__fish_use_subcommand' -a 'run' -d 'Run a security scan via backend'
|
|
119
|
+
complete -c rafter -n '__fish_use_subcommand' -a 'agent' -d 'Agent security features'
|
|
120
|
+
complete -c rafter -n '__fish_use_subcommand' -a 'ci' -d 'CI/CD integration'
|
|
121
|
+
complete -c rafter -n '__fish_use_subcommand' -a 'hook' -d 'Hook handlers'
|
|
122
|
+
complete -c rafter -n '__fish_use_subcommand' -a 'mcp' -d 'MCP server'
|
|
123
|
+
complete -c rafter -n '__fish_use_subcommand' -a 'completion' -d 'Shell completion scripts'
|
|
124
|
+
|
|
125
|
+
# agent subcommands
|
|
126
|
+
complete -c rafter -n '__fish_seen_subcommand_from agent' -a 'init scan exec config audit audit-skill install-hook verify status'
|
|
127
|
+
complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l quiet -s q -d 'Only output if secrets found'
|
|
128
|
+
complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l json -d 'JSON output'
|
|
129
|
+
complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l format -d 'Output format: text, json, sarif'
|
|
130
|
+
complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l staged -d 'Scan staged files'
|
|
131
|
+
complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcommand_from scan' -l engine -d 'Engine: gitleaks, patterns, auto'
|
|
132
|
+
|
|
133
|
+
# hook subcommands
|
|
134
|
+
complete -c rafter -n '__fish_seen_subcommand_from hook' -a 'pretool posttool'
|
|
135
|
+
|
|
136
|
+
# completion subcommands
|
|
137
|
+
complete -c rafter -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish'
|
|
138
|
+
`;
|
|
139
|
+
export function createCompletionCommand() {
|
|
140
|
+
return new Command("completion")
|
|
141
|
+
.description("Generate shell completion scripts")
|
|
142
|
+
.argument("<shell>", "Shell type: bash, zsh, or fish")
|
|
143
|
+
.addHelpText("after", `
|
|
144
|
+
Examples:
|
|
145
|
+
# bash — add to ~/.bashrc
|
|
146
|
+
eval "$(rafter completion bash)"
|
|
147
|
+
|
|
148
|
+
# zsh — add to ~/.zshrc
|
|
149
|
+
eval "$(rafter completion zsh)"
|
|
150
|
+
|
|
151
|
+
# fish — save to completions dir
|
|
152
|
+
rafter completion fish > ~/.config/fish/completions/rafter.fish
|
|
153
|
+
`)
|
|
154
|
+
.action((shell) => {
|
|
155
|
+
switch (shell.toLowerCase()) {
|
|
156
|
+
case "bash":
|
|
157
|
+
process.stdout.write(BASH_COMPLETION.trimStart());
|
|
158
|
+
break;
|
|
159
|
+
case "zsh":
|
|
160
|
+
process.stdout.write(ZSH_COMPLETION.trimStart());
|
|
161
|
+
break;
|
|
162
|
+
case "fish":
|
|
163
|
+
process.stdout.write(FISH_COMPLETION.trimStart());
|
|
164
|
+
break;
|
|
165
|
+
default:
|
|
166
|
+
console.error(`Unknown shell: ${shell}. Supported: bash, zsh, fish`);
|
|
167
|
+
process.exit(1);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createHookPretoolCommand } from "./pretool.js";
|
|
3
|
+
import { createHookPosttoolCommand } from "./posttool.js";
|
|
4
|
+
export function createHookCommand() {
|
|
5
|
+
const hook = new Command("hook")
|
|
6
|
+
.description("Hook handlers for agent platform integration");
|
|
7
|
+
hook.addCommand(createHookPretoolCommand());
|
|
8
|
+
hook.addCommand(createHookPosttoolCommand());
|
|
9
|
+
return hook;
|
|
10
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { RegexScanner } from "../../scanners/regex-scanner.js";
|
|
3
|
+
import { AuditLogger } from "../../core/audit-logger.js";
|
|
4
|
+
export function createHookPosttoolCommand() {
|
|
5
|
+
return new Command("posttool")
|
|
6
|
+
.description("PostToolUse hook handler (reads stdin, redacts secrets in output, writes JSON to stdout)")
|
|
7
|
+
.action(async () => {
|
|
8
|
+
const input = await readStdin();
|
|
9
|
+
let payload;
|
|
10
|
+
try {
|
|
11
|
+
payload = JSON.parse(input);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
writeOutput({ action: "continue" });
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const output = evaluateToolResponse(payload);
|
|
18
|
+
writeOutput(output);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function evaluateToolResponse(payload) {
|
|
22
|
+
const { tool_response } = payload;
|
|
23
|
+
// No response body — pass through
|
|
24
|
+
if (!tool_response) {
|
|
25
|
+
return { action: "continue" };
|
|
26
|
+
}
|
|
27
|
+
const scanner = new RegexScanner();
|
|
28
|
+
let modified = false;
|
|
29
|
+
const redacted = { ...tool_response };
|
|
30
|
+
// Scan and redact output
|
|
31
|
+
if (typeof tool_response.output === "string" && tool_response.output) {
|
|
32
|
+
if (scanner.hasSecrets(tool_response.output)) {
|
|
33
|
+
redacted.output = scanner.redact(tool_response.output);
|
|
34
|
+
modified = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Scan and redact content (used by some tools)
|
|
38
|
+
if (typeof tool_response.content === "string" && tool_response.content) {
|
|
39
|
+
if (scanner.hasSecrets(tool_response.content)) {
|
|
40
|
+
redacted.content = scanner.redact(tool_response.content);
|
|
41
|
+
modified = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (modified) {
|
|
45
|
+
const audit = new AuditLogger();
|
|
46
|
+
const matchCount = countMatches(scanner, tool_response);
|
|
47
|
+
audit.logContentSanitized(`${payload.tool_name} tool response`, matchCount);
|
|
48
|
+
return { action: "modify", tool_response: redacted };
|
|
49
|
+
}
|
|
50
|
+
return { action: "continue" };
|
|
51
|
+
}
|
|
52
|
+
function countMatches(scanner, tool_response) {
|
|
53
|
+
let count = 0;
|
|
54
|
+
if (typeof tool_response?.output === "string" && tool_response.output) {
|
|
55
|
+
count += scanner.scanText(tool_response.output).length;
|
|
56
|
+
}
|
|
57
|
+
if (typeof tool_response?.content === "string" && tool_response.content) {
|
|
58
|
+
count += scanner.scanText(tool_response.content).length;
|
|
59
|
+
}
|
|
60
|
+
return count;
|
|
61
|
+
}
|
|
62
|
+
function readStdin() {
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
let data = "";
|
|
65
|
+
process.stdin.setEncoding("utf-8");
|
|
66
|
+
process.stdin.on("data", (chunk) => { data += chunk; });
|
|
67
|
+
process.stdin.on("end", () => { resolve(data); });
|
|
68
|
+
process.stdin.resume();
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function writeOutput(output) {
|
|
72
|
+
process.stdout.write(JSON.stringify(output) + "\n");
|
|
73
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { CommandInterceptor } from "../../core/command-interceptor.js";
|
|
3
|
+
import { RegexScanner } from "../../scanners/regex-scanner.js";
|
|
4
|
+
import { AuditLogger } from "../../core/audit-logger.js";
|
|
5
|
+
import { execSync } from "child_process";
|
|
6
|
+
export function createHookPretoolCommand() {
|
|
7
|
+
return new Command("pretool")
|
|
8
|
+
.description("PreToolUse hook handler (reads stdin, writes JSON decision to stdout)")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const input = await readStdin();
|
|
11
|
+
let payload;
|
|
12
|
+
try {
|
|
13
|
+
payload = JSON.parse(input);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
// Can't parse → fail open
|
|
17
|
+
writeDecision({ decision: "allow" });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const decision = evaluateToolCall(payload);
|
|
21
|
+
writeDecision(decision);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
function evaluateToolCall(payload) {
|
|
25
|
+
const { tool_name, tool_input } = payload;
|
|
26
|
+
if (tool_name === "Bash") {
|
|
27
|
+
return evaluateBash(tool_input.command || "");
|
|
28
|
+
}
|
|
29
|
+
if (tool_name === "Write" || tool_name === "Edit") {
|
|
30
|
+
return evaluateWrite(tool_input);
|
|
31
|
+
}
|
|
32
|
+
return { decision: "allow" };
|
|
33
|
+
}
|
|
34
|
+
function evaluateBash(command) {
|
|
35
|
+
const interceptor = new CommandInterceptor();
|
|
36
|
+
const audit = new AuditLogger();
|
|
37
|
+
const evaluation = interceptor.evaluate(command);
|
|
38
|
+
// Blocked — hard deny
|
|
39
|
+
if (!evaluation.allowed && !evaluation.requiresApproval) {
|
|
40
|
+
audit.logCommandIntercepted(command, false, "blocked", evaluation.reason);
|
|
41
|
+
return {
|
|
42
|
+
decision: "deny",
|
|
43
|
+
reason: `Blocked by Rafter policy: ${evaluation.reason}`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Requires approval — deny (agent can't provide interactive approval)
|
|
47
|
+
if (evaluation.requiresApproval) {
|
|
48
|
+
audit.logCommandIntercepted(command, false, "blocked", evaluation.reason);
|
|
49
|
+
return {
|
|
50
|
+
decision: "deny",
|
|
51
|
+
reason: `Rafter policy requires approval: ${evaluation.reason}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Git commit/push — scan staged files for secrets
|
|
55
|
+
const trimmed = command.trim();
|
|
56
|
+
if (trimmed.startsWith("git commit") || trimmed.startsWith("git push")) {
|
|
57
|
+
const scanResult = scanStagedFiles();
|
|
58
|
+
if (scanResult.secretsFound) {
|
|
59
|
+
audit.logSecretDetected("staged files", `${scanResult.count} secret(s)`, "blocked");
|
|
60
|
+
return {
|
|
61
|
+
decision: "deny",
|
|
62
|
+
reason: `${scanResult.count} secret(s) detected in ${scanResult.files} staged file(s). Run 'rafter agent scan --staged' for details.`,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
audit.logCommandIntercepted(command, true, "allowed");
|
|
67
|
+
return { decision: "allow" };
|
|
68
|
+
}
|
|
69
|
+
function evaluateWrite(toolInput) {
|
|
70
|
+
// Write uses "content", Edit uses "new_string"
|
|
71
|
+
const content = toolInput.content || toolInput.new_string || "";
|
|
72
|
+
if (!content) {
|
|
73
|
+
return { decision: "allow" };
|
|
74
|
+
}
|
|
75
|
+
const scanner = new RegexScanner();
|
|
76
|
+
if (scanner.hasSecrets(content)) {
|
|
77
|
+
const matches = scanner.scanText(content);
|
|
78
|
+
const names = [...new Set(matches.map(m => m.pattern.name))];
|
|
79
|
+
const audit = new AuditLogger();
|
|
80
|
+
audit.logSecretDetected(toolInput.file_path || "file content", names.join(", "), "blocked");
|
|
81
|
+
return {
|
|
82
|
+
decision: "deny",
|
|
83
|
+
reason: `Secret detected in file content: ${names.join(", ")}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { decision: "allow" };
|
|
87
|
+
}
|
|
88
|
+
function scanStagedFiles() {
|
|
89
|
+
try {
|
|
90
|
+
const stagedOutput = execSync("git diff --cached --name-only --diff-filter=ACM", {
|
|
91
|
+
encoding: "utf-8",
|
|
92
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
93
|
+
}).trim();
|
|
94
|
+
if (!stagedOutput) {
|
|
95
|
+
return { secretsFound: false, count: 0, files: 0 };
|
|
96
|
+
}
|
|
97
|
+
const stagedFiles = stagedOutput.split("\n").filter(f => f.trim());
|
|
98
|
+
const scanner = new RegexScanner();
|
|
99
|
+
const results = scanner.scanFiles(stagedFiles);
|
|
100
|
+
const totalMatches = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
101
|
+
return {
|
|
102
|
+
secretsFound: results.length > 0,
|
|
103
|
+
count: totalMatches,
|
|
104
|
+
files: results.length,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { secretsFound: false, count: 0, files: 0 };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function readStdin() {
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
let data = "";
|
|
114
|
+
process.stdin.setEncoding("utf-8");
|
|
115
|
+
process.stdin.on("data", (chunk) => { data += chunk; });
|
|
116
|
+
process.stdin.on("end", () => { resolve(data); });
|
|
117
|
+
process.stdin.resume();
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function writeDecision(decision) {
|
|
121
|
+
process.stdout.write(JSON.stringify(decision) + "\n");
|
|
122
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createMcpServeCommand } from "./server.js";
|
|
3
|
+
export function createMcpCommand() {
|
|
4
|
+
const mcp = new Command("mcp")
|
|
5
|
+
.description("MCP server for cross-platform security tools");
|
|
6
|
+
mcp.addCommand(createMcpServeCommand());
|
|
7
|
+
return mcp;
|
|
8
|
+
}
|