@bugb-technologies/bravos 1.0.1 → 1.0.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/dist/cli.js +20 -3
- package/dist/guide.js +1 -1
- package/dist/plugin-release.js +212 -0
- package/dist/report/data.js +1 -1
- package/dist/setup.js +8 -6
- package/dist/skills.js +24 -0
- package/docs/GUIDE.md +1 -1
- package/hooks/lib/sast.mjs +16 -0
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import { writeOmpExtension } from './omp.js';
|
|
|
11
11
|
import { homeOf, installGlobal } from './global.js';
|
|
12
12
|
import { uninstall } from './uninstall.js';
|
|
13
13
|
import { checkKind, newToken, readAllAnswers, readDraft, renderReview, summarize, writePage } from './review.js';
|
|
14
|
-
import { spawn as spawnChild } from 'node:child_process';
|
|
14
|
+
import { spawn as spawnChild, spawnSync } from 'node:child_process';
|
|
15
15
|
import { createServer as netServer } from 'node:net';
|
|
16
16
|
import { dirname as pathDirname, join as pathJoin } from 'node:path';
|
|
17
17
|
import { OPENCODE_CONFIG, writeOpencode } from './opencode.js';
|
|
@@ -485,6 +485,23 @@ export function buildProgram(io, result) {
|
|
|
485
485
|
}
|
|
486
486
|
lines.push(JSON.stringify(opts.json ? a : a.result, null, 2));
|
|
487
487
|
}));
|
|
488
|
+
// @comment -- "bravos hook <name> runs one of the hook scripts this install ships (session-start, pre-pr, autopilot) with the given arguments, stdin passed through: what a plugin's skills call where the repository has no .bravos/hooks/ copy. Only those three names run; the script's own exit code is returned."
|
|
489
|
+
program
|
|
490
|
+
.command('hook <name> [args...]')
|
|
491
|
+
.description('Run one of bravos\'s hook scripts from this install (session-start, pre-pr, autopilot), e.g. `bravos hook session-start --briefing` or `bravos hook pre-pr --check`: what plugin installs use where the repository has no .bravos/hooks/.')
|
|
492
|
+
.allowUnknownOption()
|
|
493
|
+
.helpOption(false)
|
|
494
|
+
.action((name, args) => guarded((lines) => {
|
|
495
|
+
const known = ['session-start', 'pre-pr', 'autopilot'];
|
|
496
|
+
if (!known.includes(name))
|
|
497
|
+
throw new BravosError(`unknown hook '${name}'; use ${known.join(', ')}`);
|
|
498
|
+
const r = spawnSync(process.execPath, [pathJoin(hooksDir(), `${name}.mjs`), ...args], { env: io.env, encoding: 'utf8', stdio: ['inherit', 'pipe', 'pipe'], maxBuffer: 16 * 1024 * 1024 });
|
|
499
|
+
if (r.stdout)
|
|
500
|
+
lines.push(r.stdout.replace(/\n$/, ''));
|
|
501
|
+
if (r.stderr)
|
|
502
|
+
io.stderr(r.stderr.replace(/\n$/, ''));
|
|
503
|
+
result.code = r.status ?? 1;
|
|
504
|
+
}));
|
|
488
505
|
program
|
|
489
506
|
.command('doctor [dir]')
|
|
490
507
|
.description('Read-only check of the tools, licence, skills and hooks bravos relies on.')
|
|
@@ -859,9 +876,9 @@ export function buildProgram(io, result) {
|
|
|
859
876
|
}
|
|
860
877
|
const row = (f) => ` ${(f.severity ?? '').toLowerCase().padEnd(8)} ${f.rule ?? '?'} ${f.file ?? '?'}:${f.line ?? '?'}${f.route ? ` ${f.route}` : f.entryPoint ? ` via ${f.entryPoint}` : ''}`;
|
|
861
878
|
lines.push(`SAST run ${c.run}${c.sha ? ` at ${String(c.sha).slice(0, 9)}` : ''}${opts.all ? '' : ' (reachable only; --all for the rest)'}`);
|
|
862
|
-
lines.push('', `Joined to a claim (${c.joined.length}) — taint evidence for bravos-templatize and bravos-prove:`, ...c.joined.map((f) => `${row(f)} [${f.claimState}]`));
|
|
879
|
+
lines.push('', `Joined to a claim (${c.joined.length}) — taint evidence for bravos-templatize and bravos-prove:`, ...c.joined.map((f) => `${row(f)} [${lib.claimLabel(f.claimState)}]`));
|
|
863
880
|
lines.push('', `Not in the threat model yet (${c.unjoined.length}) — leads for bravos-annotate:`, ...c.unjoined.map(row));
|
|
864
|
-
lines.push('', `Dependencies (${c.dependencies.length}):`, ...c.dependencies.map((f) => `${row(f)} vulnerable function reached: ${
|
|
881
|
+
lines.push('', `Dependencies (${c.dependencies.length}):`, ...c.dependencies.map((f) => `${row(f)} vulnerable function reached: ${lib.reachLabel(f.vulnerableFunctionReached)}`));
|
|
865
882
|
lines.push('', `Secrets (${c.secrets.length}) — rotate; never pentest candidates:`, ...c.secrets.map((f) => ` ${f.rule ?? '?'} ${f.file ?? '?'}:${f.line ?? '?'}${f.inHistory ? ' (in git history)' : ''}`));
|
|
866
883
|
}));
|
|
867
884
|
program
|
package/dist/guide.js
CHANGED
|
@@ -120,7 +120,7 @@ export const GUIDE_TOPICS = [
|
|
|
120
120
|
{ name: 'Is this exposure real?', skills: ['bravos-templatize', 'bravos-env', 'bravos-prove'], body: ['Template → authorized target → run. Only a reproduced run confirms; reading code never does.'] },
|
|
121
121
|
{ name: 'I fixed a finding', skills: ['bravos-recheck'], body: ['Re-verifies stale claims, replays the retest kit, and checks the fix added no new exposure.'] },
|
|
122
122
|
{ name: 'I am about to open a PR', skills: ['bravos-ship'], body: ['The no-mistakes gate: validate, lint since the merge base, disclose new exposure, block confirmed exploits on changed paths.'] },
|
|
123
|
-
{ name: 'Find the routes, callers or blast radius', skills: ['bravos-codegraph'], body: ['One bounded question to the code graph instead of grepping.
|
|
123
|
+
{ name: 'Find the routes, callers or blast radius', skills: ['bravos-codegraph'], body: ['One bounded question to the code graph instead of grepping. Team plan: bravos setup --codegraph stages codegraph-mcp, then bravos graph build . indexes the repository. With it, other skills get sharper on their own: bravos-templatize targets the route and auth that actually reach each sink, bravos-recheck follows a fix\'s callers and blast radius into other files, bravos-context drafts the real public surface, and bravos-prove tests the most reachable exposures first. Without it they read the code and say so.'] },
|
|
124
124
|
{ name: 'Scan for dependency CVEs, secrets, IaC', skills: ['bravos-sast'], body: ['Licensed bravos-cli: Semgrep rules, request-to-sink taint over the code graph, dependency CVEs with whether the vulnerable function is reached, secrets (git history too) and IaC. It reads the GuardLink threat model as input, so annotate first. Says plainly when it is unavailable.'] },
|
|
125
125
|
{ name: 'What did SAST find, and what happens to it', skills: ['bravos-sast', 'bravos-annotate', 'bravos-templatize', 'bravos-prove', 'bravos-recheck'], body: ['bravos sast candidates (free, no model) reads the newest run in .bravos/sast/: reachable findings joined to a claim feed bravos-templatize and bravos-prove (the route and taint path to target, tested first); reachable findings not in the threat model are leads for bravos-annotate; bravos-recheck rescans the changed files after a fix. A static finding is a lead, never a confirmation; only a run confirms. The briefing\'s SAST line shows the latest run, or that SAST is unavailable.'] },
|
|
126
126
|
{ name: 'Keep it fresh while I work, or nightly', skills: ['bravos-schedule'], body: ['An in-session /loop from .claude/loop.md, the nightly workflow, or a cloud task you create. Loops never send test traffic.'] },
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, relative } from 'node:path';
|
|
3
|
+
import { SERVERS } from './mcp.js';
|
|
4
|
+
import { hooksDir, packageRoot, version } from './paths.js';
|
|
5
|
+
import { listBundledSkills, renderPluginSkillFile, renderPortableSkillFile, viaCliHooks } from './skills.js';
|
|
6
|
+
// @comment -- "The public plugin repository (Bugb-Technologies/bravos-plugins): one generated tree that every harness installs from. The repository root is the Gemini CLI extension and the Antigravity plugin; claude/, codex/, copilot/ and cursor/ are the other harnesses' plugins, each listed by that harness's own marketplace file at the root. It carries the skills, the session hook scripts and the guardlink MCP entry, never the CLI's source, the analyzer or the code graph, and the Bravos licence."
|
|
7
|
+
export const RELEASE_REPO = 'Bugb-Technologies/bravos-plugins';
|
|
8
|
+
const NAME = 'bravos';
|
|
9
|
+
const DESCRIPTION = 'Bravos makes your coding agent work the threat model (guardlink), authorized pentests (cxg), the code graph and SAST, with the evidence kept in your repository. Needs the bravos CLI: npm install -g @bugb-technologies/bravos.';
|
|
10
|
+
const SHORT = 'Threat model, pentests, code graph and SAST for your coding agent.';
|
|
11
|
+
const AUTHOR = { name: 'BugB Technologies', url: 'https://www.bugb.io' };
|
|
12
|
+
const HOMEPAGE = 'https://docs.bugb.io/bravos/';
|
|
13
|
+
const REPOSITORY = `https://github.com/${RELEASE_REPO}`;
|
|
14
|
+
const LICENSE_ID = 'LicenseRef-Bravos-Software-Licence';
|
|
15
|
+
const KEYWORDS = ['security', 'threat-modeling', 'guardlink', 'pentest', 'cxg', 'sast', 'code-graph', 'skills'];
|
|
16
|
+
const SESSION_MATCHER = 'startup|resume|clear|compact';
|
|
17
|
+
const json = (v) => Buffer.from(JSON.stringify(v, null, 2) + '\n');
|
|
18
|
+
const text = (s) => Buffer.from(s.endsWith('\n') ? s : s + '\n');
|
|
19
|
+
function walk(dir) {
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
+
const abs = join(dir, e.name);
|
|
23
|
+
if (e.isDirectory())
|
|
24
|
+
out.push(...walk(abs));
|
|
25
|
+
else if (e.isFile())
|
|
26
|
+
out.push(abs);
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
const guardlinkServer = () => ({ type: 'stdio', command: SERVERS.guardlink.command, args: [...SERVERS.guardlink.args] });
|
|
31
|
+
// @comment -- "Every file of the public plugin repository, computed and not written. Skills: Claude Code's copy uses ${CLAUDE_SKILL_DIR} and ${CLAUDE_PLUGIN_ROOT}, which it substitutes; every other copy uses the portable SKILL_DIR wording. In every copy a skill's `node .bravos/hooks/<hook>.mjs` is `bravos hook <hook>`, because a plugin install has no .bravos/hooks/. Session hooks are registered where the harness's plugin format and SessionStart output are known: Claude Code, Codex, Gemini CLI and Antigravity. Copilot, VS Code and Cursor get the skills and MCP, and the bravos skill loads the briefing itself."
|
|
32
|
+
export function releaseFiles(opts = {}) {
|
|
33
|
+
const v = version();
|
|
34
|
+
const files = [];
|
|
35
|
+
const add = (path, content) => files.push({ path, content });
|
|
36
|
+
const skills = listBundledSkills(opts.skillsDir);
|
|
37
|
+
const hooks = opts.hooksDir ?? hooksDir();
|
|
38
|
+
const hookFiles = walk(hooks).sort();
|
|
39
|
+
const licence = opts.licence ?? readFileSync(join(packageRoot(), 'LICENSE'));
|
|
40
|
+
const addSkills = (prefix, render) => {
|
|
41
|
+
for (const skill of skills) {
|
|
42
|
+
for (const f of skill.files) {
|
|
43
|
+
const md = f.rel.endsWith('.md');
|
|
44
|
+
add(`${prefix}skills/${skill.name}/${f.rel}`, md ? Buffer.from(render(f.content.toString('utf8'), f.rel)) : f.content);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const addHookScripts = (prefix) => {
|
|
49
|
+
for (const abs of hookFiles)
|
|
50
|
+
add(`${prefix}hooks/${relative(hooks, abs).split('\\').join('/')}`, readFileSync(abs));
|
|
51
|
+
};
|
|
52
|
+
const sessionGroup = (command) => ({ matcher: SESSION_MATCHER, hooks: [{ type: 'command', command, timeout: 30 }] });
|
|
53
|
+
const base = { name: NAME, version: v, description: DESCRIPTION, author: AUTHOR, homepage: HOMEPAGE, repository: REPOSITORY, license: LICENSE_ID, keywords: KEYWORDS };
|
|
54
|
+
// Claude Code: claude/, listed by .claude-plugin/marketplace.json.
|
|
55
|
+
addSkills('claude/', (c, rel) => viaCliHooks(renderPluginSkillFile(c, rel)));
|
|
56
|
+
addHookScripts('claude/');
|
|
57
|
+
add('claude/hooks/hooks.json', json({ hooks: { SessionStart: [sessionGroup('node "${CLAUDE_PLUGIN_ROOT}/hooks/session-start.mjs" --harness claude')] } }));
|
|
58
|
+
add('claude/.mcp.json', json({ mcpServers: { guardlink: SERVERS.guardlink } }));
|
|
59
|
+
add('claude/.claude-plugin/plugin.json', json(base));
|
|
60
|
+
add('claude/LICENSE', licence);
|
|
61
|
+
add('.claude-plugin/marketplace.json', json({
|
|
62
|
+
name: NAME,
|
|
63
|
+
owner: AUTHOR,
|
|
64
|
+
description: SHORT,
|
|
65
|
+
plugins: [{ name: NAME, source: './claude', description: DESCRIPTION, version: v, category: 'security', homepage: HOMEPAGE }],
|
|
66
|
+
}));
|
|
67
|
+
// Codex: codex/, listed by .agents/plugins/marketplace.json. Hooks at the plugin root, as Codex's own plugins keep them.
|
|
68
|
+
addSkills('codex/', renderPortableSkillFile);
|
|
69
|
+
addHookScripts('codex/');
|
|
70
|
+
add('codex/hooks.json', json({ hooks: { SessionStart: [sessionGroup('node "${PLUGIN_ROOT}/hooks/session-start.mjs" --harness codex')] } }));
|
|
71
|
+
add('codex/.mcp.json', json({ mcpServers: { guardlink: guardlinkServer() } }));
|
|
72
|
+
add('codex/.codex-plugin/plugin.json', json({
|
|
73
|
+
...base,
|
|
74
|
+
skills: './skills/',
|
|
75
|
+
mcpServers: './.mcp.json',
|
|
76
|
+
interface: {
|
|
77
|
+
displayName: 'Bravos',
|
|
78
|
+
shortDescription: SHORT,
|
|
79
|
+
longDescription: DESCRIPTION,
|
|
80
|
+
developerName: AUTHOR.name,
|
|
81
|
+
category: 'Security',
|
|
82
|
+
capabilities: ['Read', 'Write'],
|
|
83
|
+
websiteURL: HOMEPAGE,
|
|
84
|
+
defaultPrompt: ['Where do I start on security here?', 'Threat-model this repository', 'Is this PR safe to open?'],
|
|
85
|
+
},
|
|
86
|
+
}));
|
|
87
|
+
add('codex/LICENSE', licence);
|
|
88
|
+
add('.agents/plugins/marketplace.json', json({
|
|
89
|
+
name: NAME,
|
|
90
|
+
interface: { displayName: 'Bravos' },
|
|
91
|
+
plugins: [{ name: NAME, source: { source: 'local', path: './codex' }, policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' }, category: 'Security' }],
|
|
92
|
+
}));
|
|
93
|
+
// GitHub Copilot CLI and VS Code: copilot/, an Agent Plugins 1.0 plugin, listed by .github/plugin/marketplace.json.
|
|
94
|
+
addSkills('copilot/', renderPortableSkillFile);
|
|
95
|
+
add('copilot/mcp.json', json({ mcpServers: { guardlink: guardlinkServer() } }));
|
|
96
|
+
add('copilot/plugin.json', json({ $schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json', ...base }));
|
|
97
|
+
add('copilot/LICENSE', licence);
|
|
98
|
+
add('.github/plugin/marketplace.json', json({
|
|
99
|
+
name: NAME,
|
|
100
|
+
owner: AUTHOR,
|
|
101
|
+
description: SHORT,
|
|
102
|
+
plugins: [{ name: NAME, source: './copilot', description: DESCRIPTION, version: v }],
|
|
103
|
+
}));
|
|
104
|
+
// Cursor (team marketplaces): cursor/, listed by .cursor-plugin/marketplace.json.
|
|
105
|
+
addSkills('cursor/', renderPortableSkillFile);
|
|
106
|
+
add('cursor/mcp.json', json({ mcpServers: { guardlink: guardlinkServer() } }));
|
|
107
|
+
add('cursor/.cursor-plugin/plugin.json', json(base));
|
|
108
|
+
add('cursor/LICENSE', licence);
|
|
109
|
+
add('.cursor-plugin/marketplace.json', json({
|
|
110
|
+
name: NAME,
|
|
111
|
+
owner: AUTHOR,
|
|
112
|
+
description: SHORT,
|
|
113
|
+
plugins: [{ name: NAME, source: './cursor', description: DESCRIPTION, version: v }],
|
|
114
|
+
}));
|
|
115
|
+
// The repository root: the Gemini CLI extension and the Antigravity plugin share skills/ and hooks/.
|
|
116
|
+
addSkills('', renderPortableSkillFile);
|
|
117
|
+
addHookScripts('');
|
|
118
|
+
add('gemini-extension.json', json({
|
|
119
|
+
name: NAME,
|
|
120
|
+
version: v,
|
|
121
|
+
description: DESCRIPTION,
|
|
122
|
+
contextFileName: 'GEMINI.md',
|
|
123
|
+
mcpServers: { guardlink: { command: SERVERS.guardlink.command, args: [...SERVERS.guardlink.args] } },
|
|
124
|
+
}));
|
|
125
|
+
add('GEMINI.md', text(GEMINI_CONTEXT));
|
|
126
|
+
add('hooks/hooks.json', json({ hooks: { SessionStart: [sessionGroup('node "${extensionPath}${/}hooks${/}session-start.mjs" --harness gemini')] } }));
|
|
127
|
+
// Antigravity runs a plugin's hooks with the plugin root as the working directory; SessionStart takes a flat list.
|
|
128
|
+
add('.antigravity-plugin/plugin.json', json({
|
|
129
|
+
...base,
|
|
130
|
+
displayName: 'Bravos',
|
|
131
|
+
suggestedPrompts: ['Where do I start on security here?', 'Threat-model this repository', 'Is this PR safe to open?'],
|
|
132
|
+
}));
|
|
133
|
+
add('hooks.json', json({ bravos: { SessionStart: [{ type: 'command', command: 'node hooks/session-start.mjs --harness agy', timeout: 30 }] } }));
|
|
134
|
+
// Antigravity loads a plugin's context only from rules/; GEMINI.md is Gemini CLI's.
|
|
135
|
+
add('rules/bravos.md', text(GEMINI_CONTEXT));
|
|
136
|
+
add('mcp_config.json', json({ mcpServers: { guardlink: { command: SERVERS.guardlink.command, args: [...SERVERS.guardlink.args] } } }));
|
|
137
|
+
add('LICENSE', licence);
|
|
138
|
+
add('README.md', text(readme(v)));
|
|
139
|
+
return files.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
140
|
+
}
|
|
141
|
+
const GEMINI_CONTEXT = `# Bravos
|
|
142
|
+
|
|
143
|
+
This extension adds the Bravos skills: the threat model as guardlink annotations,
|
|
144
|
+
authorized pentests with cxg, the code graph and SAST. Start with the \`bravos\`
|
|
145
|
+
skill. If no briefing is in context, run \`bravos hook session-start --briefing\`
|
|
146
|
+
once to print the repository's security picture.
|
|
147
|
+
|
|
148
|
+
The skills drive the \`bravos\` CLI (\`npm install -g @bugb-technologies/bravos\`),
|
|
149
|
+
\`guardlink\` and \`cxg\`. Never send a request to a host other than loopback
|
|
150
|
+
without a committed, reviewed \`.guardlink/scope.yaml\` and the user's own typed
|
|
151
|
+
authorization naming the target.`;
|
|
152
|
+
function readme(v) {
|
|
153
|
+
return `# Bravos plugins
|
|
154
|
+
|
|
155
|
+
Bravos ${v} for the coding agent you already use. Each plugin carries the same 14
|
|
156
|
+
skills and the guardlink MCP server; the session-start briefing loads by itself
|
|
157
|
+
where the agent supports it.
|
|
158
|
+
|
|
159
|
+
**First, install the CLI** (Node 20 or newer). The skills drive it:
|
|
160
|
+
|
|
161
|
+
\`\`\`bash
|
|
162
|
+
npm install -g @bugb-technologies/bravos
|
|
163
|
+
\`\`\`
|
|
164
|
+
|
|
165
|
+
Then install the plugin for your agent:
|
|
166
|
+
|
|
167
|
+
| Agent | Install |
|
|
168
|
+
| --- | --- |
|
|
169
|
+
| Claude Code | \`claude plugin marketplace add ${RELEASE_REPO}\` then \`claude plugin install bravos@bravos\` |
|
|
170
|
+
| Codex | \`codex plugin marketplace add ${RELEASE_REPO}\` then \`/plugins\` in Codex and install Bravos |
|
|
171
|
+
| GitHub Copilot CLI | \`copilot plugin marketplace add ${RELEASE_REPO}\` then \`copilot plugin install bravos@bravos\` |
|
|
172
|
+
| VS Code (agent plugins) | add \`${RELEASE_REPO}\` to \`chat.plugins.marketplaces\`, then install Bravos from the Agent Customizations view |
|
|
173
|
+
| Gemini CLI | \`gemini extensions install ${REPOSITORY}\` |
|
|
174
|
+
| Antigravity | \`agy plugin install ${REPOSITORY}\` |
|
|
175
|
+
| Cursor (Teams and Enterprise) | Dashboard → Plugins → Import from Repo → \`${REPOSITORY}\` |
|
|
176
|
+
|
|
177
|
+
You do not need a plugin: \`bravos init\` in a repository installs the same skills,
|
|
178
|
+
hooks and MCP wiring for every agent it finds, and commits them with the code.
|
|
179
|
+
|
|
180
|
+
## Plans
|
|
181
|
+
|
|
182
|
+
The research loop is free for one researcher in one repository. The pre-PR gate
|
|
183
|
+
and autopilot, CI and nightly workflows, scheduled upkeep, workspaces, campaigns,
|
|
184
|
+
SAST and the code graph are on the Team plan: \`bravos account activate\` starts a
|
|
185
|
+
30-day trial with no card. See [how licensing works](https://docs.bugb.io/bravos/concepts/how-licensing-works/).
|
|
186
|
+
|
|
187
|
+
## Docs
|
|
188
|
+
|
|
189
|
+
[docs.bugb.io/bravos](${HOMEPAGE})
|
|
190
|
+
|
|
191
|
+
## Licence
|
|
192
|
+
|
|
193
|
+
Bravos Software Licence: see [LICENSE](LICENSE). This repository is generated from
|
|
194
|
+
the Bravos release; changes go into Bravos itself, not here.
|
|
195
|
+
`;
|
|
196
|
+
}
|
|
197
|
+
// @comment -- "Writes the public plugin repository's tree into out: removes every tracked path the previous build wrote except .git/, then writes releaseFiles(). Returns the paths written."
|
|
198
|
+
export function writeRelease(out, opts = {}) {
|
|
199
|
+
const files = releaseFiles(opts);
|
|
200
|
+
mkdirSync(out, { recursive: true });
|
|
201
|
+
for (const e of readdirSync(out)) {
|
|
202
|
+
if (e === '.git')
|
|
203
|
+
continue;
|
|
204
|
+
rmSync(join(out, e), { recursive: true, force: true });
|
|
205
|
+
}
|
|
206
|
+
for (const f of files) {
|
|
207
|
+
const abs = join(out, f.path);
|
|
208
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
209
|
+
writeFileSync(abs, f.content);
|
|
210
|
+
}
|
|
211
|
+
return files.map((f) => f.path);
|
|
212
|
+
}
|
package/dist/report/data.js
CHANGED
|
@@ -182,7 +182,7 @@ export async function gatherEvidence(root, env, opts) {
|
|
|
182
182
|
id: `SAST-${s}`,
|
|
183
183
|
kind: 'sast',
|
|
184
184
|
supports: reached ? ['code-review'] : [],
|
|
185
|
-
summary: `${group}: ${f.rule ?? '?'} ${f.severity ?? ''} ${f.file ?? '?'}:${f.line ?? '?'}${f.route ? ` (${f.route})` : ''}${group === 'dependency' ? ` vulnerable function reached: ${
|
|
185
|
+
summary: `${group}: ${f.rule ?? '?'} ${f.severity ?? ''} ${f.file ?? '?'}:${f.line ?? '?'}${f.route ? ` (${f.route})` : ''}${group === 'dependency' ? ` vulnerable function reached: ${sastLib.reachLabel(f.vulnerableFunctionReached)}` : ''}`,
|
|
186
186
|
file: f.file,
|
|
187
187
|
line: num(f.line),
|
|
188
188
|
});
|
package/dist/setup.js
CHANGED
|
@@ -74,14 +74,16 @@ export async function runSetup(env, opts, io) {
|
|
|
74
74
|
}
|
|
75
75
|
return lines;
|
|
76
76
|
}
|
|
77
|
-
// @comment -- "A staged bundle proves it runs: codegraph-build --
|
|
77
|
+
// @comment -- "A staged bundle proves it runs: codegraph-build --version, bravos-cli --version, codegraph-mcp --help (it has no --version and answers with its usage), each under the bundle's own environment. A bundle that verifies but does not run is reported, not hidden."
|
|
78
78
|
export function selfTest(kind, dir, env) {
|
|
79
79
|
const out = [];
|
|
80
|
-
const bins = kind === 'analyzer' ? [['bravos-cli', ['--version']]] : [['codegraph-build', ['--
|
|
80
|
+
const bins = kind === 'analyzer' ? [['bravos-cli', ['--version']]] : [['codegraph-build', ['--version']], ['codegraph-mcp', ['--help']]];
|
|
81
81
|
for (const [name, args] of bins) {
|
|
82
82
|
const r = spawnSync(join(dir, name), args, { env: bundleEnvironment(dir, env), encoding: 'utf8', timeout: 60000, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
83
|
-
const
|
|
84
|
-
|
|
83
|
+
const isMcp = name === 'codegraph-mcp';
|
|
84
|
+
const ok = r.status === 0 || (isMcp && (r.stdout + r.stderr).includes('usage'));
|
|
85
|
+
const said = isMcp ? 'runs' : (r.stdout || r.stderr).trim().split('\n')[0].slice(0, 80);
|
|
86
|
+
out.push(` ${ok ? '✔' : '✘'} ${name}${ok ? ` ${said}` : ` did not run: ${(r.stderr || r.error?.message || '').trim().split('\n')[0]}`}`);
|
|
85
87
|
}
|
|
86
88
|
return out;
|
|
87
89
|
}
|
|
@@ -105,8 +107,8 @@ export async function runSast(root, env, opts = {}) {
|
|
|
105
107
|
const a = ['analyze', root, '--ci', '--fail-on', 'never', '--allow-incomplete-analysis', '--codegraph', graph, '--scip', opts.scip ?? 'off', '-f', 'json,sarif', '-o', join(runDir, 'report')];
|
|
106
108
|
if (opts.scope?.length)
|
|
107
109
|
a.push('--scope', opts.scope.join(','));
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
// @comment -- ".bravos/ is always excluded: it holds the hooks bravos init copied into the repository and the SAST runs themselves, which are bravos's code, not the customer's, and would otherwise surface as findings 'not in the threat model'."
|
|
111
|
+
a.push('--exclude', ['.bravos', ...(opts.exclude ?? []).filter((p) => p !== '.bravos')].join(','));
|
|
110
112
|
if (opts.minSeverity)
|
|
111
113
|
a.push('--min-severity', opts.minSeverity);
|
|
112
114
|
if (opts.timeoutS)
|
package/dist/skills.js
CHANGED
|
@@ -68,6 +68,30 @@ export function renderPluginSkillFile(content, rel = 'SKILL.md') {
|
|
|
68
68
|
.split('{{SKILLS_ROOT}}')
|
|
69
69
|
.join('.claude/skills');
|
|
70
70
|
}
|
|
71
|
+
// @comment -- "A plugin has no .bravos/hooks/ in the repository, so a skill's `node .bravos/hooks/<hook>.mjs` becomes `bravos hook <hook>`, which runs the same script from the installed CLI."
|
|
72
|
+
export function viaCliHooks(content) {
|
|
73
|
+
return content.replace(/node \.bravos\/hooks\/([\w-]+)\.mjs/g, 'bravos hook $1');
|
|
74
|
+
}
|
|
75
|
+
/** The line a portable SKILL.md carries after its frontmatter, saying what SKILL_DIR means. */
|
|
76
|
+
export const SKILL_DIR_NOTE = '> Paths that start with `SKILL_DIR/` are files in this skill\'s own folder, the folder that holds this SKILL.md.';
|
|
77
|
+
// @comment -- "Portable plugin render, for harnesses that substitute no variable inside SKILL.md (Codex, Copilot, VS Code, Cursor, Gemini, Antigravity): {{SKILL_DIR}} becomes the literal SKILL_DIR with a note saying it is this skill's folder; {{SKILLS_ROOT}}/bravos* becomes SKILL_DIR/../bravos*, a sibling in the same plugin; any other {{SKILLS_ROOT}} (guardlink's generated skills) stays at the repo's .claude/skills; hook scripts run through the CLI."
|
|
78
|
+
export function renderPortableSkillFile(content, rel = 'SKILL.md') {
|
|
79
|
+
const needsNote = rel === 'SKILL.md' && (content.includes('{{SKILL_DIR}}') || content.includes('{{SKILLS_ROOT}}/bravos'));
|
|
80
|
+
let out = viaCliHooks(withMarker(content, rel))
|
|
81
|
+
.split('{{SKILL_DIR}}')
|
|
82
|
+
.join('SKILL_DIR')
|
|
83
|
+
.split('{{SKILLS_ROOT}}/bravos')
|
|
84
|
+
.join('SKILL_DIR/../bravos')
|
|
85
|
+
.split('{{SKILLS_ROOT}}')
|
|
86
|
+
.join('.claude/skills');
|
|
87
|
+
if (needsNote) {
|
|
88
|
+
const lines = out.split('\n');
|
|
89
|
+
const at = lines.indexOf(MARKER);
|
|
90
|
+
lines.splice(at === -1 ? 0 : at + 1, 0, '', SKILL_DIR_NOTE);
|
|
91
|
+
out = lines.join('\n');
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
71
95
|
// @comment -- "User-level render (bravos init --global): bravos's own paths become absolute under the home folder (skills in ~/.claude/skills, hook scripts in ~/.bravos/hooks), while {{SKILLS_ROOT}} for guardlink's generated skills stays at the repo's .claude/skills."
|
|
72
96
|
export function renderGlobalSkillFile(home) {
|
|
73
97
|
return (content, _harness, name, rel = 'SKILL.md') => withMarker(content, rel)
|
package/docs/GUIDE.md
CHANGED
|
@@ -144,7 +144,7 @@ The no-mistakes gate: validate, lint since the merge base, disclose new exposure
|
|
|
144
144
|
|
|
145
145
|
Skills: `/bravos-codegraph`
|
|
146
146
|
|
|
147
|
-
One bounded question to the code graph instead of grepping.
|
|
147
|
+
One bounded question to the code graph instead of grepping. Team plan: bravos setup --codegraph stages codegraph-mcp, then bravos graph build . indexes the repository. With it, other skills get sharper on their own: bravos-templatize targets the route and auth that actually reach each sink, bravos-recheck follows a fix's callers and blast radius into other files, bravos-context drafts the real public surface, and bravos-prove tests the most reachable exposures first. Without it they read the code and say so.
|
|
148
148
|
|
|
149
149
|
### Scan for dependency CVEs, secrets, IaC
|
|
150
150
|
|
package/hooks/lib/sast.mjs
CHANGED
|
@@ -111,6 +111,22 @@ export function sastCandidates(root, { all = false } = {}) {
|
|
|
111
111
|
return out;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// @comment -- "One line for the claim a SAST finding joined: the analyzer's guardlink_join is an object ({asset, threat, declared_at, …}) in current reports and was a plain state string in older ones; both read as text, never as [object Object]."
|
|
115
|
+
export function claimLabel(cs) {
|
|
116
|
+
if (cs == null) return 'claim';
|
|
117
|
+
if (typeof cs !== 'object') return String(cs);
|
|
118
|
+
const what = cs.asset && cs.threat ? `#${cs.asset} → #${cs.threat}` : cs.threat ? `#${cs.threat}` : 'claim';
|
|
119
|
+
return cs.declared_at ? `${what} at ${cs.declared_at}` : what;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// @comment -- "One phrase for whether a dependency advisory's vulnerable function is reached: the analyzer reports {state, reason, …}; older reports carried a boolean or nothing."
|
|
123
|
+
export function reachLabel(v) {
|
|
124
|
+
if (v == null) return 'unknown';
|
|
125
|
+
if (typeof v !== 'object') return String(v);
|
|
126
|
+
const state = v.state ?? 'unknown';
|
|
127
|
+
return v.reason ? `${state} (${String(v.reason).replace(/_/g, ' ')})` : state;
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
// @comment -- "The Python bravos CLI (siete) on PATH, as `bravos` or renamed to `bravos-py`, and never this package's own `bravos` bin: an npm shim is a script that execs node on dist/cli.js, and skipping it keeps the hooks pointed at the Python install's analyzer and graph until phase 3 makes this CLI carry them."
|
|
115
131
|
export function pythonBravos(env = process.env) {
|
|
116
132
|
// Its own name first, then bravos-py: the name it keeps once it is renamed so `bravos` means this CLI.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bugb-technologies/bravos",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Skills pack and installer that make Claude Code and Codex power users of guardlink (threat-model-as-code) and cert-x-gen (pentest-as-code).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"security",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"test": "vitest run",
|
|
38
38
|
"capture-help": "bash scripts/capture-help.sh",
|
|
39
39
|
"prepublishOnly": "npm run build && npm test",
|
|
40
|
-
"build:plugin": "npm run build && node scripts/build-plugin.mjs"
|
|
40
|
+
"build:plugin": "npm run build && node scripts/build-plugin.mjs",
|
|
41
|
+
"build:plugin-release": "node scripts/build-plugin-release.mjs"
|
|
41
42
|
},
|
|
42
43
|
"dependencies": {
|
|
43
44
|
"commander": "^15.0.0",
|