@rafter-security/cli 0.7.7 → 0.7.9
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 +27 -681
- package/dist/commands/agent/components.js +282 -138
- package/dist/commands/agent/init.js +399 -150
- package/dist/commands/agent/scan.js +52 -23
- package/dist/commands/agent/verify.js +211 -21
- package/dist/commands/brief.js +13 -45
- package/dist/commands/issues/from-scan.js +4 -1
- package/dist/core/config-manager.js +6 -0
- package/dist/core/custom-patterns.js +86 -4
- package/dist/core/policy-loader.js +60 -1
- package/dist/scanners/regex-scanner.js +4 -5
- package/dist/utils/skill-manager.js +96 -16
- package/package.json +1 -1
- package/resources/agents/rafter.md +81 -0
- package/resources/continue-rules/rafter-code-review.md +15 -0
- package/resources/continue-rules/rafter-secure-design.md +15 -0
- package/resources/continue-rules/rafter-skill-review.md +15 -0
- package/resources/continue-rules/rafter.md +16 -0
- package/resources/cursor-rules/rafter-code-review.mdc +14 -0
- package/resources/cursor-rules/rafter-secure-design.mdc +14 -0
- package/resources/cursor-rules/rafter-skill-review.mdc +14 -0
- package/resources/cursor-rules/rafter.mdc +15 -0
- package/resources/rafter-security-skill.md +17 -9
- package/resources/windsurf-rules/rafter-code-review.md +14 -0
- package/resources/windsurf-rules/rafter-secure-design.md +14 -0
- package/resources/windsurf-rules/rafter-skill-review.md +14 -0
- package/resources/windsurf-rules/rafter.md +15 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Load custom secret patterns from ~/.rafter/patterns/
|
|
3
|
-
* and suppression rules from .rafterignore.
|
|
3
|
+
* and suppression rules from .rafterignore + .rafter.yml ignore section.
|
|
4
4
|
*/
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path from "path";
|
|
@@ -119,12 +119,13 @@ export function loadSuppressions(projectRoot = process.cwd()) {
|
|
|
119
119
|
continue;
|
|
120
120
|
const colonIdx = line.indexOf(":");
|
|
121
121
|
if (colonIdx === -1) {
|
|
122
|
-
suppressions.push({ pathGlob: line });
|
|
122
|
+
suppressions.push({ pathGlob: line, source: ".rafterignore" });
|
|
123
123
|
}
|
|
124
124
|
else {
|
|
125
125
|
suppressions.push({
|
|
126
126
|
pathGlob: line.slice(0, colonIdx).trim(),
|
|
127
127
|
patternName: line.slice(colonIdx + 1).trim() || undefined,
|
|
128
|
+
source: ".rafterignore",
|
|
128
129
|
});
|
|
129
130
|
}
|
|
130
131
|
}
|
|
@@ -134,6 +135,79 @@ export function loadSuppressions(projectRoot = process.cwd()) {
|
|
|
134
135
|
}
|
|
135
136
|
return suppressions;
|
|
136
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Convert .rafter.yml ignore rules into the flat Suppression list used at scan time.
|
|
140
|
+
* Each entry's path globs cross-product with its rule names.
|
|
141
|
+
*/
|
|
142
|
+
export function policyIgnoreToSuppressions(rules) {
|
|
143
|
+
if (!rules || rules.length === 0)
|
|
144
|
+
return [];
|
|
145
|
+
const out = [];
|
|
146
|
+
for (const rule of rules) {
|
|
147
|
+
if (!Array.isArray(rule.paths) || rule.paths.length === 0)
|
|
148
|
+
continue;
|
|
149
|
+
const ruleNames = Array.isArray(rule.rules) && rule.rules.length > 0 ? rule.rules : [undefined];
|
|
150
|
+
for (const pathGlob of rule.paths) {
|
|
151
|
+
for (const ruleName of ruleNames) {
|
|
152
|
+
out.push({
|
|
153
|
+
pathGlob,
|
|
154
|
+
patternName: ruleName,
|
|
155
|
+
reason: rule.reason,
|
|
156
|
+
source: ".rafter.yml",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Find the first matching suppression for a finding, or null. First-match wins so
|
|
165
|
+
* users can put more specific entries earlier and rely on stable precedence.
|
|
166
|
+
*/
|
|
167
|
+
export function findSuppression(filePath, patternName, suppressions) {
|
|
168
|
+
for (const s of suppressions) {
|
|
169
|
+
if (matchGlob(s.pathGlob, filePath)) {
|
|
170
|
+
if (!s.patternName || s.patternName.toLowerCase() === patternName.toLowerCase()) {
|
|
171
|
+
return s;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Split scan results into kept matches and structured suppressed findings.
|
|
179
|
+
* Used by the scan command for engine-agnostic suppression.
|
|
180
|
+
*/
|
|
181
|
+
export function applySuppressions(results, suppressions) {
|
|
182
|
+
if (suppressions.length === 0)
|
|
183
|
+
return { results, suppressed: [] };
|
|
184
|
+
const suppressed = [];
|
|
185
|
+
const filtered = [];
|
|
186
|
+
for (const r of results) {
|
|
187
|
+
const kept = [];
|
|
188
|
+
for (const m of r.matches) {
|
|
189
|
+
const hit = findSuppression(r.file, m.pattern.name, suppressions);
|
|
190
|
+
if (hit) {
|
|
191
|
+
suppressed.push({
|
|
192
|
+
file: r.file,
|
|
193
|
+
line: m.line ?? null,
|
|
194
|
+
column: m.column ?? null,
|
|
195
|
+
rule: m.pattern.name,
|
|
196
|
+
severity: m.pattern.severity,
|
|
197
|
+
reason: hit.reason ?? null,
|
|
198
|
+
source: hit.source ?? ".rafterignore",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
kept.push(m);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (kept.length > 0) {
|
|
206
|
+
filtered.push({ ...r, matches: kept });
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { results: filtered, suppressed };
|
|
210
|
+
}
|
|
137
211
|
/**
|
|
138
212
|
* Returns true if a finding should be suppressed.
|
|
139
213
|
*/
|
|
@@ -151,10 +225,18 @@ export function isSuppressed(filePath, patternName, suppressions) {
|
|
|
151
225
|
* Match a file path against a glob pattern using minimatch.
|
|
152
226
|
*
|
|
153
227
|
* Uses `matchBase` so bare patterns like "*.env" match against the basename
|
|
154
|
-
* (e.g. "config/.env"), and `dot` so dotfiles are included.
|
|
228
|
+
* (e.g. "config/.env"), and `dot` so dotfiles are included. Also tries
|
|
229
|
+
* matching the glob against any suffix of the path so that relative globs
|
|
230
|
+
* like `tests/fixtures/**` match absolute paths under any project root.
|
|
155
231
|
*/
|
|
156
232
|
function matchGlob(glob, filePath) {
|
|
157
233
|
const g = glob.replace(/\\/g, "/");
|
|
158
234
|
const f = filePath.replace(/\\/g, "/");
|
|
159
|
-
|
|
235
|
+
if (minimatch(f, g, { dot: true, matchBase: true }))
|
|
236
|
+
return true;
|
|
237
|
+
// Auto-anchor relative globs to "anywhere in the path" so that `tests/**`
|
|
238
|
+
// matches `/abs/project/tests/foo`. Skip if the user already anchored.
|
|
239
|
+
if (g.startsWith("/") || g.startsWith("**/") || g.startsWith("**"))
|
|
240
|
+
return false;
|
|
241
|
+
return minimatch(f, "**/" + g, { dot: true });
|
|
160
242
|
}
|
|
@@ -76,6 +76,31 @@ function mapPolicy(raw) {
|
|
|
76
76
|
}));
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
if (Array.isArray(raw.ignore)) {
|
|
80
|
+
const rules = [];
|
|
81
|
+
for (const entry of raw.ignore) {
|
|
82
|
+
if (!entry || typeof entry !== "object") {
|
|
83
|
+
console.error(`Warning: skipping malformed ignore entry — must be an object with paths.`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const paths = entry.paths;
|
|
87
|
+
if (!Array.isArray(paths) || paths.length === 0) {
|
|
88
|
+
console.error(`Warning: skipping ignore entry — "paths" must be a non-empty array of strings.`);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const rule = { paths: paths.map((p) => String(p)) };
|
|
92
|
+
if (Array.isArray(entry.rules)) {
|
|
93
|
+
rule.rules = entry.rules.map((r) => String(r));
|
|
94
|
+
}
|
|
95
|
+
if (typeof entry.reason === "string" && entry.reason) {
|
|
96
|
+
rule.reason = entry.reason;
|
|
97
|
+
}
|
|
98
|
+
rules.push(rule);
|
|
99
|
+
}
|
|
100
|
+
if (rules.length > 0) {
|
|
101
|
+
policy.ignore = rules;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
79
104
|
if (raw.audit && typeof raw.audit === "object") {
|
|
80
105
|
policy.audit = {};
|
|
81
106
|
if (raw.audit.retention_days != null) {
|
|
@@ -153,7 +178,7 @@ function deriveDocId(source, kind) {
|
|
|
153
178
|
const crypto = require("crypto");
|
|
154
179
|
return crypto.createHash("sha256").update(source).digest("hex").slice(0, 8);
|
|
155
180
|
}
|
|
156
|
-
const VALID_TOP_LEVEL_KEYS = new Set(["version", "risk_level", "command_policy", "scan", "audit", "docs"]);
|
|
181
|
+
const VALID_TOP_LEVEL_KEYS = new Set(["version", "risk_level", "command_policy", "scan", "ignore", "audit", "docs"]);
|
|
157
182
|
const VALID_RISK_LEVELS = new Set(["minimal", "moderate", "aggressive"]);
|
|
158
183
|
const VALID_COMMAND_MODES = new Set(["allow-all", "approve-dangerous", "deny-list"]);
|
|
159
184
|
const VALID_LOG_LEVELS = new Set(["debug", "info", "warn", "error"]);
|
|
@@ -232,6 +257,40 @@ function validatePolicy(policy, raw) {
|
|
|
232
257
|
}
|
|
233
258
|
}
|
|
234
259
|
}
|
|
260
|
+
if (policy.ignore !== undefined) {
|
|
261
|
+
if (!Array.isArray(policy.ignore)) {
|
|
262
|
+
console.error(`Warning: "ignore" must be an array — ignoring.`);
|
|
263
|
+
delete policy.ignore;
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
const valid = [];
|
|
267
|
+
for (const entry of policy.ignore) {
|
|
268
|
+
if (!entry || typeof entry !== "object") {
|
|
269
|
+
console.error(`Warning: skipping malformed ignore entry — must be an object with paths.`);
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
if (!Array.isArray(entry.paths) || entry.paths.length === 0 || !entry.paths.every((p) => typeof p === "string" && p)) {
|
|
273
|
+
console.error(`Warning: skipping ignore entry — "paths" must be a non-empty array of strings.`);
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (entry.rules !== undefined && (!Array.isArray(entry.rules) || !entry.rules.every((r) => typeof r === "string"))) {
|
|
277
|
+
console.error(`Warning: skipping ignore entry — "rules" must be an array of strings.`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (entry.reason !== undefined && typeof entry.reason !== "string") {
|
|
281
|
+
console.error(`Warning: ignore entry "reason" must be a string — dropping reason.`);
|
|
282
|
+
delete entry.reason;
|
|
283
|
+
}
|
|
284
|
+
valid.push(entry);
|
|
285
|
+
}
|
|
286
|
+
if (valid.length > 0) {
|
|
287
|
+
policy.ignore = valid;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
delete policy.ignore;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
235
294
|
if (policy.audit) {
|
|
236
295
|
if (policy.audit.retentionDays !== undefined && (typeof policy.audit.retentionDays !== "number" || isNaN(policy.audit.retentionDays))) {
|
|
237
296
|
console.error(`Warning: "audit.retention_days" must be a number — ignoring.`);
|
|
@@ -2,7 +2,7 @@ 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
|
|
5
|
+
import { loadCustomPatterns } from "../core/custom-patterns.js";
|
|
6
6
|
export class RegexScanner {
|
|
7
7
|
constructor(customPatterns) {
|
|
8
8
|
const patterns = [...DEFAULT_SECRET_PATTERNS, ...loadCustomPatterns()];
|
|
@@ -16,16 +16,15 @@ export class RegexScanner {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
this.engine = new PatternEngine(patterns);
|
|
19
|
-
this.suppressions = loadSuppressions();
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
* Scan a single file for secrets
|
|
21
|
+
* Scan a single file for secrets. Suppression is applied at the scan
|
|
22
|
+
* command boundary (engine-agnostic), not here.
|
|
23
23
|
*/
|
|
24
24
|
scanFile(filePath) {
|
|
25
25
|
try {
|
|
26
26
|
const content = fs.readFileSync(filePath, "utf-8");
|
|
27
|
-
const
|
|
28
|
-
const matches = raw.filter((m) => !isSuppressed(filePath, m.pattern.name, this.suppressions));
|
|
27
|
+
const matches = this.engine.scanWithPosition(content);
|
|
29
28
|
return { file: filePath, matches };
|
|
30
29
|
}
|
|
31
30
|
catch (e) {
|
|
@@ -11,22 +11,51 @@ export class SkillManager {
|
|
|
11
11
|
this.configManager = new ConfigManager();
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Path to the OpenClaw root directory.
|
|
15
|
+
*
|
|
16
|
+
* The platform's presence is the platform root, not the skills dir — a
|
|
17
|
+
* fresh OpenClaw install has no skills written yet.
|
|
18
|
+
*/
|
|
19
|
+
getOpenClawRoot() {
|
|
20
|
+
return path.join(os.homedir(), ".openclaw");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Path to the OpenClaw default-workspace skills directory.
|
|
24
|
+
*
|
|
25
|
+
* Per docs.openclaw.ai/tools/skills, OpenClaw auto-discovers skills from
|
|
26
|
+
* `<workspace>/skills/<skill>/SKILL.md`. The default workspace lives at
|
|
27
|
+
* `~/.openclaw/workspace/`. ClawHub-style skills are directories
|
|
28
|
+
* containing a `SKILL.md`, not loose markdown files.
|
|
29
|
+
*
|
|
30
|
+
* Earlier rafter versions wrote to `~/.openclaw/skills/<name>.md` — that
|
|
31
|
+
* path was never read by OpenClaw at runtime. Migrated in rf-zgwj.
|
|
15
32
|
*/
|
|
16
33
|
getOpenClawSkillsDir() {
|
|
17
|
-
return path.join(
|
|
34
|
+
return path.join(this.getOpenClawRoot(), "workspace", "skills");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Path to the Rafter Security skill directory (containing SKILL.md).
|
|
38
|
+
*/
|
|
39
|
+
getRafterSkillDir() {
|
|
40
|
+
return path.join(this.getOpenClawSkillsDir(), "rafter-security");
|
|
18
41
|
}
|
|
19
42
|
/**
|
|
20
|
-
*
|
|
43
|
+
* Path to the Rafter Security SKILL.md file.
|
|
21
44
|
*/
|
|
22
45
|
getRafterSkillPath() {
|
|
23
|
-
return path.join(this.
|
|
46
|
+
return path.join(this.getRafterSkillDir(), "SKILL.md");
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Legacy install path used by rafter ≤ 0.7.7. Removed on reinstall.
|
|
50
|
+
*/
|
|
51
|
+
getLegacyRafterSkillPath() {
|
|
52
|
+
return path.join(this.getOpenClawRoot(), "skills", "rafter-security.md");
|
|
24
53
|
}
|
|
25
54
|
/**
|
|
26
55
|
* Get path to old skill-auditor (for migration)
|
|
27
56
|
*/
|
|
28
57
|
getOldSkillAuditorPath() {
|
|
29
|
-
return path.join(this.
|
|
58
|
+
return path.join(this.getOpenClawRoot(), "skills", "rafter-skill-auditor.md");
|
|
30
59
|
}
|
|
31
60
|
/**
|
|
32
61
|
* Get path to Rafter Security skill source in CLI resources
|
|
@@ -42,10 +71,21 @@ export class SkillManager {
|
|
|
42
71
|
return path.join(this.getOpenClawSkillsDir(), ".backups");
|
|
43
72
|
}
|
|
44
73
|
/**
|
|
45
|
-
* Check if OpenClaw is installed
|
|
74
|
+
* Check if OpenClaw is installed.
|
|
75
|
+
*
|
|
76
|
+
* Detects the platform root (~/.openclaw) — a fresh OpenClaw install
|
|
77
|
+
* doesn't have the workspace skills dir yet, so checking the skills dir
|
|
78
|
+
* gave a false-negative until at least one skill was written.
|
|
46
79
|
*/
|
|
47
80
|
isOpenClawInstalled() {
|
|
48
|
-
return fs.existsSync(this.
|
|
81
|
+
return fs.existsSync(this.getOpenClawRoot());
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check if the legacy skill file from rafter ≤ 0.7.7 is present.
|
|
85
|
+
* Used to print a migration note on reinstall (rf-zgwj).
|
|
86
|
+
*/
|
|
87
|
+
hasLegacyRafterSkill() {
|
|
88
|
+
return fs.existsSync(this.getLegacyRafterSkillPath());
|
|
49
89
|
}
|
|
50
90
|
/**
|
|
51
91
|
* Check if Rafter Security skill is installed
|
|
@@ -135,6 +175,36 @@ export class SkillManager {
|
|
|
135
175
|
return false;
|
|
136
176
|
}
|
|
137
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* Remove the rafter ≤ 0.7.7 install path
|
|
180
|
+
* (`~/.openclaw/skills/rafter-security.md`). OpenClaw never read that
|
|
181
|
+
* path; we strip it on reinstall so the user is left with just the
|
|
182
|
+
* canonical ClawHub-shaped skill (rf-zgwj migration).
|
|
183
|
+
*/
|
|
184
|
+
removeLegacyRafterSkill() {
|
|
185
|
+
const legacy = this.getLegacyRafterSkillPath();
|
|
186
|
+
if (!fs.existsSync(legacy))
|
|
187
|
+
return;
|
|
188
|
+
try {
|
|
189
|
+
fs.unlinkSync(legacy);
|
|
190
|
+
console.log(`✓ Removed legacy ${legacy} (superseded by ClawHub-shaped skill at ${this.getRafterSkillPath()})`);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
/* best-effort */
|
|
194
|
+
}
|
|
195
|
+
// Clean up empty parent if we just emptied it. Don't touch dirs that
|
|
196
|
+
// contain other user content.
|
|
197
|
+
const legacyDir = path.dirname(legacy);
|
|
198
|
+
try {
|
|
199
|
+
const entries = fs.readdirSync(legacyDir);
|
|
200
|
+
if (entries.length === 0) {
|
|
201
|
+
fs.rmdirSync(legacyDir);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* best-effort */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
138
208
|
/**
|
|
139
209
|
* Migrate from old separate skill-auditor to combined Rafter Security skill
|
|
140
210
|
*/
|
|
@@ -151,25 +221,33 @@ export class SkillManager {
|
|
|
151
221
|
}
|
|
152
222
|
}
|
|
153
223
|
/**
|
|
154
|
-
* Install Rafter Security skill to OpenClaw (verbose result)
|
|
224
|
+
* Install Rafter Security skill to OpenClaw (verbose result).
|
|
225
|
+
*
|
|
226
|
+
* rf-zgwj: writes to the canonical ClawHub workspace location
|
|
227
|
+
* (`~/.openclaw/workspace/skills/rafter-security/SKILL.md`) so OpenClaw
|
|
228
|
+
* auto-discovers it at session start. Earlier versions wrote to
|
|
229
|
+
* `~/.openclaw/skills/rafter-security.md` — that file is removed on
|
|
230
|
+
* reinstall as a migration step.
|
|
155
231
|
*/
|
|
156
232
|
async installRafterSkillVerbose(force = false) {
|
|
157
233
|
const skillPath = this.getRafterSkillPath();
|
|
158
234
|
const sourcePath = this.getRafterSkillSourcePath();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (!fs.existsSync(openclawDir)) {
|
|
162
|
-
return { ok: false, sourcePath, destPath: skillPath, error: `OpenClaw not found: ${openclawDir}` };
|
|
235
|
+
if (!this.isOpenClawInstalled()) {
|
|
236
|
+
return { ok: false, sourcePath, destPath: skillPath, error: `OpenClaw not found: ${this.getOpenClawRoot()}` };
|
|
163
237
|
}
|
|
164
238
|
// Check if already installed and not forcing
|
|
165
239
|
if (!force && this.isRafterSkillInstalled()) {
|
|
240
|
+
// Still strip the legacy file on reinstall — a previous rafter
|
|
241
|
+
// version may have left it behind alongside a hand-installed skill.
|
|
242
|
+
this.removeLegacyRafterSkill();
|
|
166
243
|
return { ok: true, sourcePath, destPath: skillPath };
|
|
167
244
|
}
|
|
168
245
|
try {
|
|
169
|
-
// Ensure
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
246
|
+
// Ensure the skill dir (NOT just the parent skills dir) exists; the
|
|
247
|
+
// ClawHub format is one directory per skill.
|
|
248
|
+
const skillDir = this.getRafterSkillDir();
|
|
249
|
+
if (!fs.existsSync(skillDir)) {
|
|
250
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
173
251
|
}
|
|
174
252
|
// Verify source exists
|
|
175
253
|
if (!fs.existsSync(sourcePath)) {
|
|
@@ -178,6 +256,8 @@ export class SkillManager {
|
|
|
178
256
|
// Copy skill file
|
|
179
257
|
const sourceContent = fs.readFileSync(sourcePath, "utf-8");
|
|
180
258
|
fs.writeFileSync(skillPath, sourceContent, "utf-8");
|
|
259
|
+
// Migration: strip the rafter ≤ 0.7.7 install path (rf-zgwj).
|
|
260
|
+
this.removeLegacyRafterSkill();
|
|
181
261
|
// Update config
|
|
182
262
|
const version = this.getSourceVersion();
|
|
183
263
|
if (version) {
|
package/package.json
CHANGED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rafter
|
|
3
|
+
description: Use proactively to answer "is this safe / secure / production worthy?" Delegate when about to commit, push, open a PR, or mark a security-relevant task done; when a diff touches auth, credentials, secrets, user input, SQL, shell, file paths, deserialization, or crypto; when evaluating a destructive shell command before running it; or when reviewing a third-party skill, MCP server, or agent config before installing. Returns a structured finding list with severity, location, and a one-sentence fix.
|
|
4
|
+
tools: Bash, Read, Grep
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Rafter — Security Sub-Agent
|
|
8
|
+
|
|
9
|
+
You are Rafter, a security-focused sub-agent. The calling agent has handed you a security question and is waiting for a tight, structured verdict. You do not write code. You do not commit. You do not run scanners other than `rafter`.
|
|
10
|
+
|
|
11
|
+
## Pick the right tier — DO NOT stop at "local"
|
|
12
|
+
|
|
13
|
+
Rafter ships three CLI tiers **and** four in-repo skills. They are NOT interchangeable. The local tier is the narrowest — secrets-only — and stopping there is the #1 way agents under-use rafter.
|
|
14
|
+
|
|
15
|
+
**Anti-patterns that count as "stopping at local":**
|
|
16
|
+
- Running `rafter secrets` clean and reporting "rafter passed" — you ran a regex sweep, not a security scan.
|
|
17
|
+
- Skipping `rafter run` because `RAFTER_API_KEY` is unset, and not saying so in the verdict.
|
|
18
|
+
- Treating the CLI as the whole tool. The judgment-layer skills (`rafter-code-review`, `rafter-secure-design`, `rafter-skill-review`) catch what scanners can't.
|
|
19
|
+
- Auditing a third-party skill / MCP / agent config by eye instead of running `rafter-skill-review`.
|
|
20
|
+
|
|
21
|
+
### CLI tiers (run code through the scanner)
|
|
22
|
+
|
|
23
|
+
1. **`rafter run`** (default mode) — remote SAST + SCA + secrets via the Rafter API. Real code analysis: dataflow, taint, vulnerable deps, crypto misuse, injection sinks. Needs `RAFTER_API_KEY`. **This is the default for "is this safe / secure / production worthy?".**
|
|
24
|
+
2. **`rafter run --mode plus`** — agentic deep-dive on suspicious patterns. Slower, higher signal. Use when fast mode flags something worth investigating, or when stakes are high (auth, payments, ingress, crypto, anything user-data-shaped).
|
|
25
|
+
3. **`rafter secrets [path]`** — local secrets only (regex + gitleaks for hardcoded API keys, tokens, private keys). Fast, offline, no key. **NOT a code security scan.** Will not find SQL injection, SSRF, auth bugs, deserialization, or logic flaws. Use only when no API key is available, or as a fast pre-check alongside `rafter run`.
|
|
26
|
+
|
|
27
|
+
If `RAFTER_API_KEY` is unset, run `rafter secrets` and **say so explicitly in your verdict** — "secrets-only pass; full code analysis was skipped (no API key)." Do not claim the code was "scanned" without that qualification. Never silently downgrade.
|
|
28
|
+
|
|
29
|
+
### Rafter skills (the judgment layer the scanner can't reach)
|
|
30
|
+
|
|
31
|
+
The CLI finds patterns. Skills ask the questions patterns miss — design choices, code-review walkthroughs, third-party-asset vetting. Skills ship next to this sub-agent at `.claude/skills/<name>/`. **`Read` the SKILL.md first; pull a sub-doc from `docs/` only if the skill points you at one.** The CLI is necessary but rarely sufficient — for any non-trivial security question, plan to use both.
|
|
32
|
+
|
|
33
|
+
- **`rafter`** — the tier router. Same three CLI tiers plus a Choose-Your-Adventure for "scan code", "evaluate a command", "audit a plugin", "understand a finding", "write secure code from scratch", "analyse existing code for flaws". Start here when the right move isn't obvious.
|
|
34
|
+
- → `.claude/skills/rafter/SKILL.md`
|
|
35
|
+
- Sub-docs: `docs/backend.md` (fast vs plus, auth, cost), `docs/cli-reference.md` (full flag matrix), `docs/finding-triage.md` (how to read output), `docs/guardrails.md` (PreToolUse hooks + risk tiers), `docs/shift-left.md` (when to invoke earlier).
|
|
36
|
+
- **`rafter-secure-design`** — shift-left, design-phase questions *before the code exists*. Use at feature kickoff, architecture review, or when picking between primitives.
|
|
37
|
+
- → `.claude/skills/rafter-secure-design/SKILL.md`
|
|
38
|
+
- Sub-docs: `docs/auth.md`, `docs/data-storage.md`, `docs/api-design.md`, `docs/ingestion.md`, `docs/deployment.md`, `docs/dependencies.md`, `docs/threat-modeling.md`, `docs/standards-pointers.md`.
|
|
39
|
+
- **`rafter-code-review`** — structured review (OWASP / MITRE / ASVS) as questions, not audits. Pairs with `rafter run`: the scanner finds known-bad patterns, this skill asks the questions patterns miss. Use during PR review, refactoring risky modules, or pre-release hardening.
|
|
40
|
+
- → `.claude/skills/rafter-code-review/SKILL.md`
|
|
41
|
+
- Sub-docs: `docs/web-app.md`, `docs/api.md`, `docs/llm.md` (LLM-integrated apps), `docs/cwe-top25.md`, `docs/asvs.md`, `docs/investigation-playbook.md`.
|
|
42
|
+
- **`rafter-skill-review`** — REQUIRED before installing any third-party `SKILL.md`, MCP manifest, Cursor rule, or agent config. Installing a skill grants Read/Bash/network under the caller's identity — `curl | sh` in a different costume. Wraps `rafter skill review`.
|
|
43
|
+
- → `.claude/skills/rafter-skill-review/SKILL.md`
|
|
44
|
+
- Sub-docs: `docs/authorship-provenance.md`, `docs/malware-indicators.md`, `docs/prompt-injection.md`, `docs/data-practices.md`, `docs/telemetry.md`, `docs/changelog-review.md`.
|
|
45
|
+
|
|
46
|
+
### Routing rule
|
|
47
|
+
|
|
48
|
+
| Question shape | Reach for |
|
|
49
|
+
|---|---|
|
|
50
|
+
| "Is this code / diff / repo safe?" (existing code) | `rafter run` (CLI tier 1) **and** `rafter-code-review` skill for the judgment layer — not one or the other |
|
|
51
|
+
| "Is this design / primitive / API shape safe?" (no code yet) | `rafter-secure-design` skill (CLI can't help — there's no code) |
|
|
52
|
+
| "Is this command safe to run?" | `rafter agent exec --dry-run -- <cmd>` (see `rafter/docs/guardrails.md`) |
|
|
53
|
+
| "Is this skill / MCP / agent config safe to install?" | `rafter-skill-review` skill — **vet before install, not after** |
|
|
54
|
+
| "How do I read this finding?" | `rafter` skill → `docs/finding-triage.md` |
|
|
55
|
+
| "Which rafter thing should I even use?" | `rafter` skill (tier router) |
|
|
56
|
+
|
|
57
|
+
## Other rafter commands you can use
|
|
58
|
+
|
|
59
|
+
- `rafter agent exec --dry-run -- <command>` — classify a shell command's risk tier before running it.
|
|
60
|
+
- `rafter agent exec -- <command>` — wrap execution; blocks on critical, prompts on high.
|
|
61
|
+
- `cat ~/.rafter/audit.jsonl` — recent security-relevant events on this machine (read-only inspection).
|
|
62
|
+
|
|
63
|
+
## Protocol
|
|
64
|
+
|
|
65
|
+
1. **Infer scope** from the caller's prompt: a path, a diff, a commit range, a shell command, a third-party config to install, a design sketch. If scope is ambiguous, default to scanning the current working directory.
|
|
66
|
+
2. **Pick the right tool** using the routing table above. When unsure, `Read` `.claude/skills/rafter/SKILL.md` first — that's the tier router.
|
|
67
|
+
3. **Run it.** Capture stdout/stderr. For skill-driven judgment work, walk the skill's checklist and capture findings the same way you would CLI output.
|
|
68
|
+
4. **Report.** One short paragraph of verdict, then findings as a list:
|
|
69
|
+
- `severity` (critical / high / medium / low / info)
|
|
70
|
+
- `location` (`file:line` or command/snippet)
|
|
71
|
+
- `rule` or category (e.g. `hardcoded-secret`, `sql-injection`, `dangerous-shell`, `design:auth-primitive`, `skill:prompt-injection`)
|
|
72
|
+
- `fix` (one sentence — what the caller should change)
|
|
73
|
+
- If there are no findings: say so in one line and stop.
|
|
74
|
+
|
|
75
|
+
## Hard rules
|
|
76
|
+
|
|
77
|
+
- **Never** modify code, write files, run `git commit`, or open PRs. You are read-only.
|
|
78
|
+
- **Never** invoke non-rafter scanners (no `npm audit`, no `safety`, no `trivy`). The caller can do that — your job is the rafter signal.
|
|
79
|
+
- **Never** silently fall back to a weaker tier. If you couldn't run the tier the question called for, name the tier you ran and why.
|
|
80
|
+
- **Never** install or load a third-party skill / MCP / agent asset on the caller's behalf. Vet via `rafter-skill-review` and return the verdict; the caller decides.
|
|
81
|
+
- Be terse. The calling agent wants a verdict, not an essay.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rafter-code-review
|
|
3
|
+
description: "REQUIRED before declaring a task done when the diff touches user input, SQL, shell, auth, credentials, file paths, serialization, or dependency surface. Walks OWASP/ASVS/MITRE questions the compiler and the test suite won't catch. Pair with `rafter run` on the same diff. A security-relevant task marked complete without both has been under-delivered — do not claim done."
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# rafter-code-review — structured pre-merge review
|
|
8
|
+
|
|
9
|
+
Pointer rule. Use the `rafter-code-review` skill at `.claude/skills/rafter-code-review/SKILL.md` BEFORE declaring done on any diff touching the surface listed above.
|
|
10
|
+
|
|
11
|
+
- Read the matching sub-doc (web-app / api / llm / cli-library-iac).
|
|
12
|
+
- Walk the OWASP / ASVS / MITRE questions the scanner can't ask.
|
|
13
|
+
- Pair with `rafter run` on the same diff — the scanner finds known-bad patterns, this skill asks the questions patterns miss.
|
|
14
|
+
|
|
15
|
+
A security-relevant task marked complete without BOTH the skill walkthrough and a `rafter run` pass has been under-delivered. Do not claim done until both are recorded.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rafter-secure-design
|
|
3
|
+
description: "REQUIRED before writing code for any feature touching auth, payments, credentials, tokens, sessions, file upload, user data, or data deletion. Invoke it, record answers inline in the design doc or PR description, then write the code. Skipping this is how auth bypass, token leakage, and delete-the-wrong-record bugs ship. If the feature touches any of those surfaces and this hasn't run, the design is incomplete — do not proceed to implementation."
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# rafter-secure-design — shift-left design review
|
|
8
|
+
|
|
9
|
+
Pointer rule. Use the `rafter-secure-design` skill at `.claude/skills/rafter-secure-design/SKILL.md` BEFORE writing code for any feature on the surface listed above.
|
|
10
|
+
|
|
11
|
+
- Read the matching sub-doc (auth / data-storage / api-design / ingestion / deployment / dependencies).
|
|
12
|
+
- Each sub-doc is a set of questions a security engineer would ask at kickoff — answer them inline in the design or PR description.
|
|
13
|
+
- Pair with `rafter-code-review` (mid-lifecycle) and `rafter run` (detection) once code exists.
|
|
14
|
+
|
|
15
|
+
Skipping this skill on the listed surfaces is how auth bypass, token leakage, and delete-the-wrong-record bugs ship. Do not proceed to implementation until it has run.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rafter-skill-review
|
|
3
|
+
description: "REQUIRED before copying any third-party SKILL.md, MCP manifest, Cursor rule, or agent config into this machine or forwarding one to a user. Installing a skill grants Read/Bash/network under your identity — this is `curl | sh` in a different costume. Do not install, load, recommend, or forward any third-party agent asset until this skill has vetted it. Bypassing this turns the agent into a delivery vector."
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# rafter-skill-review — vet before you install
|
|
8
|
+
|
|
9
|
+
Pointer rule. Use the `rafter-skill-review` skill at `.claude/skills/rafter-skill-review/SKILL.md` BEFORE installing or forwarding any third-party agent asset.
|
|
10
|
+
|
|
11
|
+
- Run `rafter skill review <path-or-url>` against the SKILL.md / MCP manifest / Cursor rule / agent config.
|
|
12
|
+
- Read the skill's sub-docs for the deeper review questions (telemetry, allowed-tools, network egress).
|
|
13
|
+
- Do NOT install, load, recommend, or forward the asset until the skill has produced a verdict.
|
|
14
|
+
|
|
15
|
+
Installing a skill grants Read/Bash/network under your identity — `curl | sh` in a different costume. Bypassing this turns the agent into a delivery vector.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: rafter
|
|
3
|
+
description: "Entry point for rafter. Invoke when a sub-skill is unclear, or when the task needs `rafter run` (remote SAST+SCA), `rafter secrets` (local secrets-only), `rafter audit`, policy checks, or command-risk evaluation. If a task is security-relevant and no rafter skill or CLI call has been made, invoke this before handing the task off — an un-evaluated \"done\" on security-relevant work is not done."
|
|
4
|
+
alwaysApply: false
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# rafter — security toolkit router
|
|
8
|
+
|
|
9
|
+
Pointer rule. Use the `rafter` skill (full guidance at `.claude/skills/rafter/SKILL.md`).
|
|
10
|
+
|
|
11
|
+
- Run `rafter run` for the default tier — remote SAST + SCA + secrets. Needs `RAFTER_API_KEY`.
|
|
12
|
+
- Run `rafter run --mode plus` for agentic deep-dive on suspicious patterns.
|
|
13
|
+
- Run `rafter secrets <path>` for offline secrets-only (NOT a code security scan).
|
|
14
|
+
- Run `rafter agent exec --dry-run -- <cmd>` to classify a shell command's risk before running it.
|
|
15
|
+
|
|
16
|
+
If unsure which tier to pick, Read `.claude/skills/rafter/SKILL.md` and follow the routing table.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "REQUIRED before declaring a task done when the diff touches user input, SQL, shell, auth, credentials, file paths, serialization, or dependency surface. Walks OWASP/ASVS/MITRE questions the compiler and the test suite won't catch. Pair with `rafter run` on the same diff. A security-relevant task marked complete without both has been under-delivered — do not claim done."
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rafter-code-review — structured pre-merge review
|
|
7
|
+
|
|
8
|
+
Pointer rule. Use the `rafter-code-review` skill at `.claude/skills/rafter-code-review/SKILL.md` BEFORE declaring done on any diff touching the surface listed above.
|
|
9
|
+
|
|
10
|
+
- Read the matching sub-doc (web-app / api / llm / cli-library-iac).
|
|
11
|
+
- Walk the OWASP / ASVS / MITRE questions the scanner can't ask.
|
|
12
|
+
- Pair with `rafter run` on the same diff — the scanner finds known-bad patterns, this skill asks the questions patterns miss.
|
|
13
|
+
|
|
14
|
+
A security-relevant task marked complete without BOTH the skill walkthrough and a `rafter run` pass has been under-delivered. Do not claim done until both are recorded.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "REQUIRED before writing code for any feature touching auth, payments, credentials, tokens, sessions, file upload, user data, or data deletion. Invoke it, record answers inline in the design doc or PR description, then write the code. Skipping this is how auth bypass, token leakage, and delete-the-wrong-record bugs ship. If the feature touches any of those surfaces and this hasn't run, the design is incomplete — do not proceed to implementation."
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rafter-secure-design — shift-left design review
|
|
7
|
+
|
|
8
|
+
Pointer rule. Use the `rafter-secure-design` skill at `.claude/skills/rafter-secure-design/SKILL.md` BEFORE writing code for any feature on the surface listed above.
|
|
9
|
+
|
|
10
|
+
- Read the matching sub-doc (auth / data-storage / api-design / ingestion / deployment / dependencies).
|
|
11
|
+
- Each sub-doc is a set of questions a security engineer would ask at kickoff — answer them inline in the design or PR description.
|
|
12
|
+
- Pair with `rafter-code-review` (mid-lifecycle) and `rafter run` (detection) once code exists.
|
|
13
|
+
|
|
14
|
+
Skipping this skill on the listed surfaces is how auth bypass, token leakage, and delete-the-wrong-record bugs ship. Do not proceed to implementation until it has run.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "REQUIRED before copying any third-party SKILL.md, MCP manifest, Cursor rule, or agent config into this machine or forwarding one to a user. Installing a skill grants Read/Bash/network under your identity — this is `curl | sh` in a different costume. Do not install, load, recommend, or forward any third-party agent asset until this skill has vetted it. Bypassing this turns the agent into a delivery vector."
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rafter-skill-review — vet before you install
|
|
7
|
+
|
|
8
|
+
Pointer rule. Use the `rafter-skill-review` skill at `.claude/skills/rafter-skill-review/SKILL.md` BEFORE installing or forwarding any third-party agent asset.
|
|
9
|
+
|
|
10
|
+
- Run `rafter skill review <path-or-url>` against the SKILL.md / MCP manifest / Cursor rule / agent config.
|
|
11
|
+
- Read the skill's sub-docs for the deeper review questions (telemetry, allowed-tools, network egress).
|
|
12
|
+
- Do NOT install, load, recommend, or forward the asset until the skill has produced a verdict.
|
|
13
|
+
|
|
14
|
+
Installing a skill grants Read/Bash/network under your identity — `curl | sh` in a different costume. Bypassing this turns the agent into a delivery vector.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Entry point for rafter. Invoke when a sub-skill is unclear, or when the task needs `rafter run` (remote SAST+SCA), `rafter secrets` (local secrets-only), `rafter audit`, policy checks, or command-risk evaluation. If a task is security-relevant and no rafter skill or CLI call has been made, invoke this before handing the task off — an un-evaluated \"done\" on security-relevant work is not done."
|
|
3
|
+
alwaysApply: false
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# rafter — security toolkit router
|
|
7
|
+
|
|
8
|
+
Pointer rule. Use the `rafter` skill (full guidance at `.claude/skills/rafter/SKILL.md`, also installed as a Cursor sub-agent at `.cursor/agents/rafter.md`).
|
|
9
|
+
|
|
10
|
+
- Run `rafter run` for the default tier — remote SAST + SCA + secrets. Needs `RAFTER_API_KEY`.
|
|
11
|
+
- Run `rafter run --mode plus` for agentic deep-dive on suspicious patterns.
|
|
12
|
+
- Run `rafter secrets <path>` for offline secrets-only (NOT a code security scan).
|
|
13
|
+
- Run `rafter agent exec --dry-run -- <cmd>` to classify a shell command's risk before running it.
|
|
14
|
+
|
|
15
|
+
If unsure which tier to pick, Read `.claude/skills/rafter/SKILL.md` and follow the routing table.
|