@bugb-technologies/bravos 1.0.0 → 1.0.2
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 +2 -2
- package/dist/guide.js +1 -1
- package/dist/report/data.js +1 -1
- package/dist/setup.js +32 -18
- package/docs/GUIDE.md +1 -1
- package/hooks/lib/sast.mjs +16 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -859,9 +859,9 @@ export function buildProgram(io, result) {
|
|
|
859
859
|
}
|
|
860
860
|
const row = (f) => ` ${(f.severity ?? '').toLowerCase().padEnd(8)} ${f.rule ?? '?'} ${f.file ?? '?'}:${f.line ?? '?'}${f.route ? ` ${f.route}` : f.entryPoint ? ` via ${f.entryPoint}` : ''}`;
|
|
861
861
|
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}]`));
|
|
862
|
+
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
863
|
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: ${
|
|
864
|
+
lines.push('', `Dependencies (${c.dependencies.length}):`, ...c.dependencies.map((f) => `${row(f)} vulnerable function reached: ${lib.reachLabel(f.vulnerableFunctionReached)}`));
|
|
865
865
|
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
866
|
}));
|
|
867
867
|
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.'] },
|
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
|
@@ -3,7 +3,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { bundleEnvironment, hostTarget, install, missingPaths, staged, VERSIONS } from './artifacts.js';
|
|
6
|
-
import { graphBin } from './codegraph.js';
|
|
6
|
+
import { graphBin, graphEnv } from './codegraph.js';
|
|
7
7
|
import { whichBin } from './guardlink.js';
|
|
8
8
|
import { findAnalyzer } from './graphbin.js';
|
|
9
9
|
import { licenceLib, requireFeature } from './licence.js';
|
|
@@ -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
|
}
|
|
@@ -100,20 +102,32 @@ export async function runSast(root, env, opts = {}) {
|
|
|
100
102
|
const runDir = join(base, run);
|
|
101
103
|
mkdirSync(runDir, { recursive: true });
|
|
102
104
|
const sha = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: root, encoding: 'utf8', timeout: 5000 }).stdout?.trim() || null;
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
105
|
+
let codegraph = opts.codegraph ?? (graphBin('codegraph-mcp', env) ? 'reuse' : 'off');
|
|
106
|
+
const argvFor = (graph) => {
|
|
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')];
|
|
108
|
+
if (opts.scope?.length)
|
|
109
|
+
a.push('--scope', opts.scope.join(','));
|
|
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(','));
|
|
112
|
+
if (opts.minSeverity)
|
|
113
|
+
a.push('--min-severity', opts.minSeverity);
|
|
114
|
+
if (opts.timeoutS)
|
|
115
|
+
a.push('--timeout', String(Math.max(1, opts.timeoutS - 1)));
|
|
116
|
+
return a;
|
|
117
|
+
};
|
|
118
|
+
// @comment -- "The analyzer reads the code graph from the same store bravos graph build writes to, so it runs with BRAVOS_CODEGRAPH_DIR set (graphEnv): without it, --codegraph reuse looks in the analyzer's own default store and finds no graph even when one was just built."
|
|
119
|
+
const runEnv = dir ? bundleEnvironment(dir, graphEnv(env)) : graphEnv(env);
|
|
120
|
+
const spawn = (graph) => spawnSync(bin, argvFor(graph), { cwd: root, env: runEnv, encoding: 'utf8', timeout: (opts.timeoutS ?? 1800) * 1000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
121
|
+
let r = spawn(codegraph);
|
|
122
|
+
const notes = [];
|
|
123
|
+
// No graph built for this commit yet: reuse refuses outright. Scan without reachability rather than fail, and say how to get it.
|
|
124
|
+
if (codegraph === 'reuse' && r.status === 3 && /CodeGraph unavailable|No CodeGraph/i.test(`${r.stdout}\n${r.stderr}`)) {
|
|
125
|
+
notes.push(' no code graph for this commit yet, so the scan ran without reachability; bravos graph build . --scip no-exec first, then bravos sast run again, to see which findings are reached');
|
|
126
|
+
codegraph = 'off';
|
|
127
|
+
r = spawn(codegraph);
|
|
128
|
+
}
|
|
115
129
|
writeFileSync(join(runDir, 'meta.json'), JSON.stringify({ sha, base: null, scope: opts.scope?.length ? opts.scope.join(',') : 'all', exit: r.status, at: new Date().toISOString(), analyzer: bin, codegraph }, null, 2) + '\n');
|
|
116
|
-
const lines = [`SAST run ${run}: bravos-cli exited ${r.status ?? 'by signal'} (0 clean at the level, 1 findings, 3 licence refused or analysis failed, 4 a stage failed)`, ` report: ${join(runDir, 'report.json')}
|
|
130
|
+
const lines = [`SAST run ${run}: bravos-cli exited ${r.status ?? 'by signal'} (0 clean at the level, 1 findings, 3 licence refused or analysis failed, 4 a stage failed)`, ` report: ${join(runDir, 'report.json')}`, ...notes];
|
|
117
131
|
if (r.status !== 0 && r.status !== 1)
|
|
118
132
|
lines.push(` ${(r.stderr || r.stdout || '').trim().split('\n').slice(-3).join('\n ')}`);
|
|
119
133
|
lines.push(' next: bravos sast candidates');
|
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.2",
|
|
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",
|