@rafter-security/cli 0.6.6 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -10
- package/dist/commands/agent/audit-skill.js +22 -20
- package/dist/commands/agent/audit.js +27 -0
- package/dist/commands/agent/components.js +800 -0
- package/dist/commands/agent/config.js +2 -1
- package/dist/commands/agent/disable.js +47 -0
- package/dist/commands/agent/enable.js +50 -0
- package/dist/commands/agent/exec.js +2 -0
- package/dist/commands/agent/index.js +6 -0
- package/dist/commands/agent/init.js +162 -163
- package/dist/commands/agent/install-hook.js +15 -14
- package/dist/commands/agent/list.js +72 -0
- package/dist/commands/agent/scan.js +4 -3
- package/dist/commands/agent/verify.js +1 -1
- package/dist/commands/backend/run.js +12 -3
- package/dist/commands/backend/scan-status.js +3 -2
- package/dist/commands/brief.js +22 -2
- package/dist/commands/ci/init.js +25 -21
- package/dist/commands/completion.js +4 -3
- package/dist/commands/docs/index.js +18 -0
- package/dist/commands/docs/list.js +37 -0
- package/dist/commands/docs/show.js +64 -0
- package/dist/commands/mcp/server.js +84 -0
- package/dist/commands/report.js +42 -41
- package/dist/commands/scan/index.js +7 -5
- package/dist/commands/skill/index.js +14 -0
- package/dist/commands/skill/install.js +89 -0
- package/dist/commands/skill/list.js +79 -0
- package/dist/commands/skill/registry.js +273 -0
- package/dist/commands/skill/remote.js +333 -0
- package/dist/commands/skill/review.js +975 -0
- package/dist/commands/skill/uninstall.js +65 -0
- package/dist/core/audit-logger.js +262 -21
- package/dist/core/config-manager.js +3 -0
- package/dist/core/docs-loader.js +148 -0
- package/dist/core/policy-loader.js +72 -1
- package/dist/core/risk-rules.js +16 -3
- package/dist/index.js +19 -9
- package/dist/scanners/gitleaks.js +6 -2
- package/package.json +1 -1
- package/resources/skills/rafter/SKILL.md +77 -97
- package/resources/skills/rafter/docs/backend.md +106 -0
- package/resources/skills/rafter/docs/cli-reference.md +199 -0
- package/resources/skills/rafter/docs/finding-triage.md +79 -0
- package/resources/skills/rafter/docs/guardrails.md +91 -0
- package/resources/skills/rafter/docs/shift-left.md +64 -0
- package/resources/skills/rafter-agent-security/SKILL.md +1 -1
- package/resources/skills/rafter-code-review/SKILL.md +91 -0
- package/resources/skills/rafter-code-review/docs/api.md +90 -0
- package/resources/skills/rafter-code-review/docs/asvs.md +120 -0
- package/resources/skills/rafter-code-review/docs/cwe-top25.md +78 -0
- package/resources/skills/rafter-code-review/docs/investigation-playbook.md +101 -0
- package/resources/skills/rafter-code-review/docs/llm.md +87 -0
- package/resources/skills/rafter-code-review/docs/web-app.md +84 -0
- package/resources/skills/rafter-secure-design/SKILL.md +103 -0
- package/resources/skills/rafter-secure-design/docs/api-design.md +97 -0
- package/resources/skills/rafter-secure-design/docs/auth.md +67 -0
- package/resources/skills/rafter-secure-design/docs/data-storage.md +90 -0
- package/resources/skills/rafter-secure-design/docs/dependencies.md +101 -0
- package/resources/skills/rafter-secure-design/docs/deployment.md +104 -0
- package/resources/skills/rafter-secure-design/docs/ingestion.md +98 -0
- package/resources/skills/rafter-secure-design/docs/standards-pointers.md +102 -0
- package/resources/skills/rafter-secure-design/docs/threat-modeling.md +128 -0
- package/resources/skills/rafter-skill-review/SKILL.md +106 -0
- package/resources/skills/rafter-skill-review/docs/authorship-provenance.md +82 -0
- package/resources/skills/rafter-skill-review/docs/changelog-review.md +99 -0
- package/resources/skills/rafter-skill-review/docs/data-practices.md +88 -0
- package/resources/skills/rafter-skill-review/docs/malware-indicators.md +79 -0
- package/resources/skills/rafter-skill-review/docs/prompt-injection.md +85 -0
- package/resources/skills/rafter-skill-review/docs/telemetry.md +78 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { snapshotComponents } from "./components.js";
|
|
3
|
+
import { fmt } from "../../utils/formatter.js";
|
|
4
|
+
/**
|
|
5
|
+
* `rafter agent list` — machine-readable inventory of everything rafter has touched
|
|
6
|
+
* (or could touch) on this machine. One row per (platform, component).
|
|
7
|
+
*
|
|
8
|
+
* Exit codes: 0 on success.
|
|
9
|
+
*/
|
|
10
|
+
export function createListCommand() {
|
|
11
|
+
return new Command("list")
|
|
12
|
+
.description("List agent integration components and their state")
|
|
13
|
+
.option("--json", "Output machine-readable JSON")
|
|
14
|
+
.option("--installed", "Only show components that are currently installed")
|
|
15
|
+
.option("--detected", "Only show components whose platform is detected")
|
|
16
|
+
.action((opts) => {
|
|
17
|
+
let rows = snapshotComponents();
|
|
18
|
+
if (opts.installed)
|
|
19
|
+
rows = rows.filter((r) => r.installed);
|
|
20
|
+
if (opts.detected)
|
|
21
|
+
rows = rows.filter((r) => r.detected);
|
|
22
|
+
if (opts.json) {
|
|
23
|
+
const payload = {
|
|
24
|
+
components: rows.map((r) => ({
|
|
25
|
+
id: r.id,
|
|
26
|
+
platform: r.platform,
|
|
27
|
+
kind: r.kind,
|
|
28
|
+
description: r.description,
|
|
29
|
+
path: r.path,
|
|
30
|
+
state: r.state,
|
|
31
|
+
installed: r.installed,
|
|
32
|
+
detected: r.detected,
|
|
33
|
+
configEnabled: r.configEnabled,
|
|
34
|
+
})),
|
|
35
|
+
};
|
|
36
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const byPlatform = new Map();
|
|
40
|
+
for (const r of rows) {
|
|
41
|
+
const arr = byPlatform.get(r.platform) ?? [];
|
|
42
|
+
arr.push(r);
|
|
43
|
+
byPlatform.set(r.platform, arr);
|
|
44
|
+
}
|
|
45
|
+
console.log(fmt.header("Rafter agent components"));
|
|
46
|
+
console.log(fmt.divider());
|
|
47
|
+
for (const [platform, list] of byPlatform) {
|
|
48
|
+
const detected = list[0]?.detected ?? false;
|
|
49
|
+
const suffix = detected ? "" : " (not detected)";
|
|
50
|
+
console.log(`\n${platform}${suffix}`);
|
|
51
|
+
for (const r of list) {
|
|
52
|
+
const label = r.id.padEnd(28);
|
|
53
|
+
let marker;
|
|
54
|
+
switch (r.state) {
|
|
55
|
+
case "installed":
|
|
56
|
+
marker = "● installed";
|
|
57
|
+
break;
|
|
58
|
+
case "not-installed":
|
|
59
|
+
marker = "○ not installed";
|
|
60
|
+
break;
|
|
61
|
+
case "not-detected":
|
|
62
|
+
default:
|
|
63
|
+
marker = "· platform not detected";
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
console.log(` ${label} ${marker}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
console.log();
|
|
70
|
+
console.log(fmt.info("Use `rafter agent enable <id>` or `rafter agent disable <id>` to toggle individual components."));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
@@ -47,6 +47,7 @@ export function createScanCommand() {
|
|
|
47
47
|
.option("--engine <engine>", "Scan engine: gitleaks or patterns", "auto")
|
|
48
48
|
.option("--baseline", "Filter findings present in the saved baseline")
|
|
49
49
|
.option("--watch", "Watch for file changes and re-scan on change")
|
|
50
|
+
.option("--history", "Scan git history for secrets (requires gitleaks engine)")
|
|
50
51
|
.action(async (scanPath, opts) => {
|
|
51
52
|
// Validate flags before doing any work
|
|
52
53
|
const validEngines = ["auto", "gitleaks", "patterns"];
|
|
@@ -103,7 +104,7 @@ export function createScanCommand() {
|
|
|
103
104
|
if (!opts.quiet) {
|
|
104
105
|
console.error(`Scanning directory: ${resolvedPath} (${engine})`);
|
|
105
106
|
}
|
|
106
|
-
results = await scanDirectory(resolvedPath, engine, scanCfg);
|
|
107
|
+
results = await scanDirectory(resolvedPath, engine, scanCfg, opts.history);
|
|
107
108
|
}
|
|
108
109
|
else {
|
|
109
110
|
if (!opts.quiet) {
|
|
@@ -372,11 +373,11 @@ async function scanFile(filePath, engine, scanCfg) {
|
|
|
372
373
|
/**
|
|
373
374
|
* Scan a directory with selected engine
|
|
374
375
|
*/
|
|
375
|
-
async function scanDirectory(dirPath, engine, scanCfg) {
|
|
376
|
+
async function scanDirectory(dirPath, engine, scanCfg, history) {
|
|
376
377
|
if (engine === "gitleaks") {
|
|
377
378
|
try {
|
|
378
379
|
const gitleaks = new GitleaksScanner();
|
|
379
|
-
return await gitleaks.scanDirectory(dirPath);
|
|
380
|
+
return await gitleaks.scanDirectory(dirPath, { useGit: history ?? false });
|
|
380
381
|
}
|
|
381
382
|
catch (e) {
|
|
382
383
|
console.error(fmt.warning("Gitleaks scan failed, falling back to patterns"));
|
|
@@ -193,7 +193,7 @@ export function createVerifyCommand() {
|
|
|
193
193
|
console.log(fmt.success(`${passed.length}/${results.length} core checks passed${warnNote}`));
|
|
194
194
|
}
|
|
195
195
|
else {
|
|
196
|
-
console.log(fmt.error(`${
|
|
196
|
+
console.log(fmt.error(`${passed.length}/${results.length} checks passed — ${hardFailed.length} failed`));
|
|
197
197
|
}
|
|
198
198
|
console.log();
|
|
199
199
|
if (hardFailed.length > 0) {
|
|
@@ -9,6 +9,7 @@ import { handleScanStatus } from "./scan-status.js";
|
|
|
9
9
|
*/
|
|
10
10
|
export async function runRemoteScan(opts) {
|
|
11
11
|
const key = resolveKey(opts.apiKey);
|
|
12
|
+
const ghToken = opts.githubToken || process.env.RAFTER_GITHUB_TOKEN;
|
|
12
13
|
let repo, branch;
|
|
13
14
|
try {
|
|
14
15
|
({ repo, branch } = detectRepo({ repo: opts.repo, branch: opts.branch, quiet: opts.quiet }));
|
|
@@ -22,10 +23,17 @@ export async function runRemoteScan(opts) {
|
|
|
22
23
|
}
|
|
23
24
|
process.exit(EXIT_GENERAL_ERROR);
|
|
24
25
|
}
|
|
26
|
+
const body = {
|
|
27
|
+
repository_name: repo,
|
|
28
|
+
branch_name: branch,
|
|
29
|
+
scan_mode: opts.mode ?? "fast",
|
|
30
|
+
};
|
|
31
|
+
if (ghToken)
|
|
32
|
+
body.github_token = ghToken;
|
|
25
33
|
if (!opts.quiet) {
|
|
26
34
|
const spinner = ora("Submitting scan").start();
|
|
27
35
|
try {
|
|
28
|
-
const { data } = await axios.post(`${API}/static/scan`,
|
|
36
|
+
const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
|
|
29
37
|
spinner.succeed(`Scan ID: ${data.scan_id}`);
|
|
30
38
|
if (opts.skipInteractive)
|
|
31
39
|
return;
|
|
@@ -56,7 +64,7 @@ export async function runRemoteScan(opts) {
|
|
|
56
64
|
}
|
|
57
65
|
else {
|
|
58
66
|
try {
|
|
59
|
-
const { data } = await axios.post(`${API}/static/scan`,
|
|
67
|
+
const { data } = await axios.post(`${API}/static/scan`, body, { headers: { "x-api-key": key } });
|
|
60
68
|
if (opts.skipInteractive)
|
|
61
69
|
return;
|
|
62
70
|
const exitCode = await handleScanStatus(data.scan_id, { "x-api-key": key }, opts.format ?? "md", opts.quiet);
|
|
@@ -90,12 +98,13 @@ function addRunOptions(cmd) {
|
|
|
90
98
|
.option("-k, --api-key <key>", "API key or RAFTER_API_KEY env var")
|
|
91
99
|
.option("-f, --format <format>", "json | md", "md")
|
|
92
100
|
.option("-m, --mode <mode>", "scan mode: fast | plus", "fast")
|
|
101
|
+
.option("--github-token <token>", "GitHub PAT for private repos (or RAFTER_GITHUB_TOKEN env var)")
|
|
93
102
|
.option("--skip-interactive", "do not wait for scan to complete")
|
|
94
103
|
.option("--quiet", "suppress status messages");
|
|
95
104
|
}
|
|
96
105
|
export function createRunCommand() {
|
|
97
106
|
return addRunOptions(new Command("run")
|
|
98
|
-
.description("Trigger a remote
|
|
107
|
+
.description("Trigger a remote security scan")).action(async (opts) => {
|
|
99
108
|
await runRemoteScan(opts);
|
|
100
109
|
});
|
|
101
110
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import axios from "axios";
|
|
2
2
|
import ora from "ora";
|
|
3
3
|
import { API, writePayload, EXIT_GENERAL_ERROR, EXIT_SCAN_NOT_FOUND } from "../../utils/api.js";
|
|
4
|
+
import { fmt as output } from "../../utils/formatter.js";
|
|
4
5
|
export async function handleScanStatus(scan_id, headers, fmt, quiet) {
|
|
5
6
|
// First poll
|
|
6
7
|
let poll;
|
|
@@ -9,10 +10,10 @@ export async function handleScanStatus(scan_id, headers, fmt, quiet) {
|
|
|
9
10
|
}
|
|
10
11
|
catch (e) {
|
|
11
12
|
if (e.response?.status === 404) {
|
|
12
|
-
console.error(`Scan '${scan_id}' not found`);
|
|
13
|
+
console.error(output.error(`Scan '${scan_id}' not found`));
|
|
13
14
|
return EXIT_SCAN_NOT_FOUND;
|
|
14
15
|
}
|
|
15
|
-
console.error(
|
|
16
|
+
console.error(output.error(`${e.response?.data || e.message}`));
|
|
16
17
|
return EXIT_GENERAL_ERROR;
|
|
17
18
|
}
|
|
18
19
|
let status = poll.data.status;
|
package/dist/commands/brief.js
CHANGED
|
@@ -9,6 +9,16 @@ function loadSkill(name) {
|
|
|
9
9
|
// Strip YAML frontmatter
|
|
10
10
|
return raw.replace(/^---[\s\S]*?---\n*/, "").trim();
|
|
11
11
|
}
|
|
12
|
+
function loadSkillDoc(skill, doc) {
|
|
13
|
+
return readFileSync(join(RESOURCES_DIR, skill, "docs", `${doc}.md`), "utf-8").trim();
|
|
14
|
+
}
|
|
15
|
+
const RAFTER_SUBDOCS = [
|
|
16
|
+
{ slug: "cli-reference", desc: "Full rafter CLI tree by category" },
|
|
17
|
+
{ slug: "guardrails", desc: "PreToolUse hooks, risk tiers, overrides" },
|
|
18
|
+
{ slug: "backend", desc: "Remote fast vs plus, setup, cost/latency" },
|
|
19
|
+
{ slug: "shift-left", desc: "Pointers to secure-design & code-review skills" },
|
|
20
|
+
{ slug: "finding-triage", desc: "How to read a finding and decide next steps" },
|
|
21
|
+
];
|
|
12
22
|
function extractSections(content, headings) {
|
|
13
23
|
const lines = content.split("\n");
|
|
14
24
|
const sections = [];
|
|
@@ -54,7 +64,7 @@ function buildTopics() {
|
|
|
54
64
|
render: () => loadSkill("rafter-agent-security"),
|
|
55
65
|
},
|
|
56
66
|
scanning: {
|
|
57
|
-
description: "Remote SAST/SCA code analysis via
|
|
67
|
+
description: "Remote SAST/SCA code analysis via Rafter API",
|
|
58
68
|
render: () => loadSkill("rafter"),
|
|
59
69
|
},
|
|
60
70
|
commands: {
|
|
@@ -78,7 +88,7 @@ function buildTopics() {
|
|
|
78
88
|
return [
|
|
79
89
|
"# Rafter Command Reference",
|
|
80
90
|
"",
|
|
81
|
-
"##
|
|
91
|
+
"## Remote Code Analysis",
|
|
82
92
|
"",
|
|
83
93
|
backCmds,
|
|
84
94
|
"",
|
|
@@ -165,6 +175,13 @@ function buildTopics() {
|
|
|
165
175
|
"the tools developers use every day.",
|
|
166
176
|
].join("\n"),
|
|
167
177
|
},
|
|
178
|
+
...Object.fromEntries(RAFTER_SUBDOCS.map(({ slug, desc }) => [
|
|
179
|
+
slug,
|
|
180
|
+
{
|
|
181
|
+
description: desc,
|
|
182
|
+
render: () => loadSkillDoc("rafter", slug),
|
|
183
|
+
},
|
|
184
|
+
])),
|
|
168
185
|
all: {
|
|
169
186
|
description: "Everything — full security + scanning + setup briefing",
|
|
170
187
|
render: () => {
|
|
@@ -503,6 +520,9 @@ function renderTopicList(topics) {
|
|
|
503
520
|
lines.push(" rafter brief commands # full command reference");
|
|
504
521
|
lines.push(" rafter brief setup/claude-code # Claude Code setup guide");
|
|
505
522
|
lines.push(" rafter brief setup/generic # setup for any agent");
|
|
523
|
+
lines.push(" rafter brief cli-reference # full CLI tree");
|
|
524
|
+
lines.push(" rafter brief guardrails # hooks + risk tiers");
|
|
525
|
+
lines.push(" rafter brief finding-triage # interpret findings");
|
|
506
526
|
lines.push(" rafter brief all # everything");
|
|
507
527
|
return lines.join("\n");
|
|
508
528
|
}
|
package/dist/commands/ci/init.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import fs from "fs";
|
|
3
3
|
import path from "path";
|
|
4
|
-
import { fmt } from "../../utils/formatter.js";
|
|
4
|
+
import { fmt, isAgentMode } from "../../utils/formatter.js";
|
|
5
5
|
export function createCiInitCommand() {
|
|
6
6
|
return new Command("init")
|
|
7
7
|
.description("Generate CI/CD pipeline config for secret scanning")
|
|
8
8
|
.option("--platform <platform>", "CI platform: github, gitlab, circleci (default: auto-detect)")
|
|
9
9
|
.option("--output <path>", "Output file path (default: platform-specific)")
|
|
10
|
-
.option("--with-
|
|
10
|
+
.option("--with-remote", "Include remote security audit job (requires RAFTER_API_KEY)")
|
|
11
|
+
.option("--with-backend", "Deprecated: use --with-remote")
|
|
11
12
|
.action((opts) => {
|
|
12
13
|
const platform = opts.platform || detectPlatform();
|
|
13
14
|
if (!platform) {
|
|
@@ -21,7 +22,8 @@ export function createCiInitCommand() {
|
|
|
21
22
|
console.error(`Valid options: ${validPlatforms.join(", ")}`);
|
|
22
23
|
process.exit(1);
|
|
23
24
|
}
|
|
24
|
-
const
|
|
25
|
+
const includeRemote = !!(opts.includeRemote || opts.withBackend);
|
|
26
|
+
const { content, defaultPath } = generateTemplate(platform, includeRemote);
|
|
25
27
|
const outputPath = opts.output || defaultPath;
|
|
26
28
|
const outputDir = path.dirname(outputPath);
|
|
27
29
|
if (!fs.existsSync(outputDir)) {
|
|
@@ -29,28 +31,30 @@ export function createCiInitCommand() {
|
|
|
29
31
|
}
|
|
30
32
|
fs.writeFileSync(outputPath, content, "utf-8");
|
|
31
33
|
console.log(fmt.success(`Generated ${platform} CI config at ${outputPath}`));
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
if (!isAgentMode()) {
|
|
35
|
+
console.log();
|
|
36
|
+
console.log("Next steps:");
|
|
37
|
+
console.log(` 1. Review the generated file: ${outputPath}`);
|
|
38
|
+
if (includeRemote) {
|
|
39
|
+
if (platform === "github") {
|
|
40
|
+
console.log(" 2. Add RAFTER_API_KEY to repo Settings > Secrets > Actions");
|
|
41
|
+
}
|
|
42
|
+
else if (platform === "gitlab") {
|
|
43
|
+
console.log(" 2. Add RAFTER_API_KEY to Settings > CI/CD > Variables");
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.log(" 2. Add RAFTER_API_KEY to project environment variables");
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
|
-
|
|
43
|
-
|
|
49
|
+
console.log(` ${includeRemote ? "3" : "2"}. Commit and push to trigger the pipeline`);
|
|
50
|
+
if (platform === "github") {
|
|
51
|
+
console.log();
|
|
52
|
+
console.log("Alternatives:");
|
|
53
|
+
console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v1");
|
|
54
|
+
console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
|
|
44
55
|
}
|
|
45
|
-
}
|
|
46
|
-
console.log(` ${opts.withBackend ? "3" : "2"}. Commit and push to trigger the pipeline`);
|
|
47
|
-
if (platform === "github") {
|
|
48
56
|
console.log();
|
|
49
|
-
console.log("Alternatives:");
|
|
50
|
-
console.log(" - GitHub Action: uses: Raftersecurity/rafter-cli@v1");
|
|
51
|
-
console.log(" - Pre-commit: https://github.com/Raftersecurity/rafter-cli#pre-commit-framework");
|
|
52
57
|
}
|
|
53
|
-
console.log();
|
|
54
58
|
});
|
|
55
59
|
}
|
|
56
60
|
function detectPlatform() {
|
|
@@ -68,7 +68,7 @@ _rafter_completions() {
|
|
|
68
68
|
if [[ "\${COMP_WORDS[1]}" == "agent" ]]; then
|
|
69
69
|
COMPREPLY=( $(compgen -W "--risk-level --with-openclaw --with-claude-code --with-codex --with-gemini --with-aider --with-cursor --with-windsurf --with-continue --with-gitleaks --all --help" -- "\${cur}") )
|
|
70
70
|
elif [[ "\${COMP_WORDS[1]}" == "ci" ]]; then
|
|
71
|
-
COMPREPLY=( $(compgen -W "--platform --output --with-backend --help" -- "\${cur}") )
|
|
71
|
+
COMPREPLY=( $(compgen -W "--platform --output --with-remote --with-backend --help" -- "\${cur}") )
|
|
72
72
|
fi
|
|
73
73
|
return 0
|
|
74
74
|
;;
|
|
@@ -81,7 +81,7 @@ const ZSH_COMPLETION = `#compdef rafter
|
|
|
81
81
|
_rafter() {
|
|
82
82
|
local -a commands
|
|
83
83
|
commands=(
|
|
84
|
-
'run:Submit a security scan
|
|
84
|
+
'run:Submit a remote security scan'
|
|
85
85
|
'scan:Alias for run'
|
|
86
86
|
'get:Retrieve scan results'
|
|
87
87
|
'usage:Check API usage quota'
|
|
@@ -369,7 +369,8 @@ complete -c rafter -n '__fish_seen_subcommand_from agent; and __fish_seen_subcom
|
|
|
369
369
|
complete -c rafter -n '__fish_seen_subcommand_from ci' -a init -d 'Initialize CI pipeline'
|
|
370
370
|
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l platform -d 'CI platform' -ra 'github gitlab circleci'
|
|
371
371
|
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l output -d 'Output path' -r
|
|
372
|
-
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-
|
|
372
|
+
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-remote -d 'Include remote audit'
|
|
373
|
+
complete -c rafter -n '__fish_seen_subcommand_from ci; and __fish_seen_subcommand_from init' -l with-backend -d 'Deprecated: use --with-remote'
|
|
373
374
|
|
|
374
375
|
# hook subcommands
|
|
375
376
|
complete -c rafter -n '__fish_seen_subcommand_from hook' -a pretool -d 'PreToolUse hook handler'
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createDocsListCommand } from "./list.js";
|
|
3
|
+
import { createDocsShowCommand } from "./show.js";
|
|
4
|
+
export function createDocsCommand() {
|
|
5
|
+
const docs = new Command("docs")
|
|
6
|
+
.description("Repo-specific security docs declared in .rafter.yml")
|
|
7
|
+
.addHelpText("after", [
|
|
8
|
+
"",
|
|
9
|
+
"Examples:",
|
|
10
|
+
" $ rafter docs list",
|
|
11
|
+
" $ rafter docs list --tag owasp",
|
|
12
|
+
" $ rafter docs show secure-coding",
|
|
13
|
+
" $ rafter docs show secure-coding --refresh",
|
|
14
|
+
].join("\n"));
|
|
15
|
+
docs.addCommand(createDocsListCommand());
|
|
16
|
+
docs.addCommand(createDocsShowCommand());
|
|
17
|
+
return docs;
|
|
18
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { listDocs } from "../../core/docs-loader.js";
|
|
3
|
+
export function createDocsListCommand() {
|
|
4
|
+
return new Command("list")
|
|
5
|
+
.description("List security docs declared in .rafter.yml")
|
|
6
|
+
.option("--tag <tag>", "Filter to docs matching this tag")
|
|
7
|
+
.option("--json", "Output as JSON")
|
|
8
|
+
.action((opts) => {
|
|
9
|
+
const entries = listDocs().filter(d => !opts.tag || (Array.isArray(d.tags) && d.tags.includes(opts.tag)));
|
|
10
|
+
if (entries.length === 0) {
|
|
11
|
+
if (opts.json) {
|
|
12
|
+
process.stdout.write("[]\n");
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
process.stderr.write("No docs configured in .rafter.yml\n");
|
|
16
|
+
}
|
|
17
|
+
process.exit(3);
|
|
18
|
+
}
|
|
19
|
+
if (opts.json) {
|
|
20
|
+
process.stdout.write(JSON.stringify(entries.map(e => ({
|
|
21
|
+
id: e.id,
|
|
22
|
+
source: e.source,
|
|
23
|
+
source_kind: e.sourceKind,
|
|
24
|
+
description: e.description || "",
|
|
25
|
+
tags: e.tags || [],
|
|
26
|
+
cache_status: e.cacheStatus,
|
|
27
|
+
})), null, 2) + "\n");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
for (const e of entries) {
|
|
31
|
+
const tags = (e.tags && e.tags.length) ? ` [${e.tags.join(", ")}]` : "";
|
|
32
|
+
const cache = e.sourceKind === "url" ? ` (${e.cacheStatus})` : "";
|
|
33
|
+
const desc = e.description ? ` — ${e.description}` : "";
|
|
34
|
+
process.stdout.write(`${e.id} ${e.source}${cache}${tags}${desc}\n`);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { resolveDocSelector, fetchDoc } from "../../core/docs-loader.js";
|
|
3
|
+
export function createDocsShowCommand() {
|
|
4
|
+
return new Command("show")
|
|
5
|
+
.description("Print the content of a doc by id or tag")
|
|
6
|
+
.argument("<id-or-tag>", "Doc id or tag selector")
|
|
7
|
+
.option("--refresh", "Force re-fetch for URL-backed docs (bypass cache)")
|
|
8
|
+
.option("--json", "Output as JSON (array of { id, source, content })")
|
|
9
|
+
.action(async (selector, opts) => {
|
|
10
|
+
const entries = resolveDocSelector(selector);
|
|
11
|
+
if (entries.length === 0) {
|
|
12
|
+
const { loadPolicy } = await import("../../core/policy-loader.js");
|
|
13
|
+
const policy = loadPolicy();
|
|
14
|
+
if (!policy || !policy.docs || policy.docs.length === 0) {
|
|
15
|
+
process.stderr.write("No docs configured in .rafter.yml\n");
|
|
16
|
+
process.exit(3);
|
|
17
|
+
}
|
|
18
|
+
process.stderr.write(`No doc matched id or tag: ${selector}\n`);
|
|
19
|
+
process.exit(2);
|
|
20
|
+
}
|
|
21
|
+
const results = [];
|
|
22
|
+
let anyError = false;
|
|
23
|
+
for (const entry of entries) {
|
|
24
|
+
try {
|
|
25
|
+
const fetched = await fetchDoc(entry, { refresh: opts.refresh });
|
|
26
|
+
results.push({
|
|
27
|
+
id: entry.id,
|
|
28
|
+
source: fetched.source,
|
|
29
|
+
source_kind: fetched.sourceKind,
|
|
30
|
+
stale: fetched.stale,
|
|
31
|
+
content: fetched.content,
|
|
32
|
+
});
|
|
33
|
+
if (fetched.stale) {
|
|
34
|
+
process.stderr.write(`Warning: ${entry.id} served from stale cache (fetch failed)\n`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
anyError = true;
|
|
39
|
+
process.stderr.write(`Error: failed to fetch ${entry.id}: ${err.message || err}\n`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (results.length === 0) {
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
if (opts.json) {
|
|
46
|
+
process.stdout.write(JSON.stringify(results, null, 2) + "\n");
|
|
47
|
+
}
|
|
48
|
+
else if (results.length === 1) {
|
|
49
|
+
process.stdout.write(results[0].content);
|
|
50
|
+
if (!results[0].content.endsWith("\n"))
|
|
51
|
+
process.stdout.write("\n");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
for (const r of results) {
|
|
55
|
+
process.stdout.write(`\n===== ${r.id} (${r.source}) =====\n`);
|
|
56
|
+
process.stdout.write(r.content);
|
|
57
|
+
if (!r.content.endsWith("\n"))
|
|
58
|
+
process.stdout.write("\n");
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (anyError)
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -7,6 +7,7 @@ import { GitleaksScanner } from "../../scanners/gitleaks.js";
|
|
|
7
7
|
import { CommandInterceptor } from "../../core/command-interceptor.js";
|
|
8
8
|
import { AuditLogger } from "../../core/audit-logger.js";
|
|
9
9
|
import { ConfigManager } from "../../core/config-manager.js";
|
|
10
|
+
import { listDocs, resolveDocSelector, fetchDoc } from "../../core/docs-loader.js";
|
|
10
11
|
import { createRequire } from "module";
|
|
11
12
|
const _require = createRequire(import.meta.url);
|
|
12
13
|
const { version: CLI_VERSION } = _require("../../../package.json");
|
|
@@ -87,6 +88,28 @@ export function createServer() {
|
|
|
87
88
|
},
|
|
88
89
|
},
|
|
89
90
|
},
|
|
91
|
+
{
|
|
92
|
+
name: "list_docs",
|
|
93
|
+
description: "List repo-specific security docs declared in .rafter.yml. Call this early in any security-relevant task to discover project-specific rules, threat models, or compliance policies the user expects agents to follow.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
tag: { type: "string", description: "Filter to docs whose tags include this value" },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: "get_doc",
|
|
103
|
+
description: "Return the content of a repo-specific security doc by id or tag. Use after list_docs to read a specific document.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
id_or_tag: { type: "string", description: "Doc id or tag selector" },
|
|
108
|
+
refresh: { type: "boolean", description: "Force re-fetch for URL-backed docs (bypass cache)" },
|
|
109
|
+
},
|
|
110
|
+
required: ["id_or_tag"],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
90
113
|
],
|
|
91
114
|
}));
|
|
92
115
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -149,6 +172,44 @@ export function createServer() {
|
|
|
149
172
|
const value = key ? manager.get(key) : manager.load();
|
|
150
173
|
return textResult(value);
|
|
151
174
|
}
|
|
175
|
+
case "list_docs": {
|
|
176
|
+
const tag = args?.tag;
|
|
177
|
+
const entries = listDocs().filter(d => !tag || (Array.isArray(d.tags) && d.tags.includes(tag)));
|
|
178
|
+
return textResult(entries.map(e => ({
|
|
179
|
+
id: e.id,
|
|
180
|
+
source: e.source,
|
|
181
|
+
source_kind: e.sourceKind,
|
|
182
|
+
description: e.description || "",
|
|
183
|
+
tags: e.tags || [],
|
|
184
|
+
cache_status: e.cacheStatus,
|
|
185
|
+
})));
|
|
186
|
+
}
|
|
187
|
+
case "get_doc": {
|
|
188
|
+
const selector = args?.id_or_tag;
|
|
189
|
+
if (!selector)
|
|
190
|
+
return errorResult("id_or_tag is required");
|
|
191
|
+
const matches = resolveDocSelector(selector);
|
|
192
|
+
if (matches.length === 0)
|
|
193
|
+
return errorResult(`No doc matched id or tag: ${selector}`);
|
|
194
|
+
const refresh = Boolean(args?.refresh);
|
|
195
|
+
const results = [];
|
|
196
|
+
for (const entry of matches) {
|
|
197
|
+
try {
|
|
198
|
+
const fetched = await fetchDoc(entry, { refresh });
|
|
199
|
+
results.push({
|
|
200
|
+
id: entry.id,
|
|
201
|
+
source: fetched.source,
|
|
202
|
+
source_kind: fetched.sourceKind,
|
|
203
|
+
stale: fetched.stale,
|
|
204
|
+
content: fetched.content,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
return errorResult(`Failed to fetch ${entry.id}: ${err.message || err}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return textResult(results);
|
|
212
|
+
}
|
|
152
213
|
default:
|
|
153
214
|
return errorResult(`Unknown tool: ${name}`);
|
|
154
215
|
}
|
|
@@ -168,6 +229,12 @@ export function createServer() {
|
|
|
168
229
|
description: "Active security policy (merged .rafter.yml + config)",
|
|
169
230
|
mimeType: "application/json",
|
|
170
231
|
},
|
|
232
|
+
{
|
|
233
|
+
uri: "rafter://docs",
|
|
234
|
+
name: "Rafter Docs",
|
|
235
|
+
description: "Repo-specific security docs declared in .rafter.yml (metadata only, no content)",
|
|
236
|
+
mimeType: "application/json",
|
|
237
|
+
},
|
|
171
238
|
],
|
|
172
239
|
}));
|
|
173
240
|
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
@@ -190,6 +257,23 @@ export function createServer() {
|
|
|
190
257
|
text: JSON.stringify(manager.loadWithPolicy(), null, 2),
|
|
191
258
|
}],
|
|
192
259
|
};
|
|
260
|
+
case "rafter://docs": {
|
|
261
|
+
const entries = listDocs().map(e => ({
|
|
262
|
+
id: e.id,
|
|
263
|
+
source: e.source,
|
|
264
|
+
source_kind: e.sourceKind,
|
|
265
|
+
description: e.description || "",
|
|
266
|
+
tags: e.tags || [],
|
|
267
|
+
cache_status: e.cacheStatus,
|
|
268
|
+
}));
|
|
269
|
+
return {
|
|
270
|
+
contents: [{
|
|
271
|
+
uri: "rafter://docs",
|
|
272
|
+
mimeType: "application/json",
|
|
273
|
+
text: JSON.stringify(entries, null, 2),
|
|
274
|
+
}],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
193
277
|
default:
|
|
194
278
|
throw new Error(`Unknown resource: ${uri}`);
|
|
195
279
|
}
|