@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 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: ${String(f.vulnerableFunctionReached ?? 'unknown')}`));
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. Needs codegraph-mcp (pip install bravos; licence-free). 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.'] },
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.'] },
@@ -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: ${String(f.vulnerableFunctionReached ?? 'unknown')}` : ''}`,
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 --ladder, bravos-cli --version, each under the bundle's own environment. A bundle that verifies but does not run is reported, not hidden."
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', ['--ladder']], ['codegraph-mcp', ['--help']]];
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 ok = r.status === 0 || (name === 'codegraph-mcp' && (r.stdout + r.stderr).includes('usage'));
84
- out.push(` ${ok ? '✔' : '✘'} ${name}${ok ? ` ${(r.stdout || r.stderr).trim().split('\n')[0].slice(0, 80)}` : ` did not run: ${(r.stderr || r.error?.message || '').trim().split('\n')[0]}`}`);
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
- const codegraph = opts.codegraph ?? (graphBin('codegraph-mcp', env) ? 'reuse' : 'off');
104
- const argv = ['analyze', root, '--ci', '--fail-on', 'never', '--allow-incomplete-analysis', '--codegraph', codegraph, '--scip', opts.scip ?? 'off', '-f', 'json,sarif', '-o', join(runDir, 'report')];
105
- if (opts.scope?.length)
106
- argv.push('--scope', opts.scope.join(','));
107
- if (opts.exclude?.length)
108
- argv.push('--exclude', opts.exclude.join(','));
109
- if (opts.minSeverity)
110
- argv.push('--min-severity', opts.minSeverity);
111
- if (opts.timeoutS)
112
- argv.push('--timeout', String(Math.max(1, opts.timeoutS - 1)));
113
- const runEnv = dir ? bundleEnvironment(dir, env) : env;
114
- const r = spawnSync(bin, argv, { cwd: root, env: runEnv, encoding: 'utf8', timeout: (opts.timeoutS ?? 1800) * 1000, maxBuffer: 64 * 1024 * 1024, stdio: ['ignore', 'pipe', 'pipe'] });
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. Needs codegraph-mcp (pip install bravos; licence-free). 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.
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
 
@@ -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.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",