@bugb-technologies/bravos 0.0.0-stage → 1.0.0

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.
Files changed (85) hide show
  1. package/LICENSE +70 -0
  2. package/README.md +225 -2
  3. package/dist/account.js +140 -0
  4. package/dist/artifacts.js +450 -0
  5. package/dist/ci.js +52 -0
  6. package/dist/cli.js +963 -0
  7. package/dist/codegraph.js +268 -0
  8. package/dist/cursor.js +70 -0
  9. package/dist/detect.js +26 -0
  10. package/dist/doctor.js +99 -0
  11. package/dist/global.js +95 -0
  12. package/dist/graphbin.js +136 -0
  13. package/dist/guardlink.js +123 -0
  14. package/dist/guide.js +255 -0
  15. package/dist/help.js +334 -0
  16. package/dist/hooks.js +167 -0
  17. package/dist/install.js +209 -0
  18. package/dist/licence.js +41 -0
  19. package/dist/lock.js +50 -0
  20. package/dist/mcp.js +139 -0
  21. package/dist/migrate.js +275 -0
  22. package/dist/omp.js +46 -0
  23. package/dist/opencode.js +118 -0
  24. package/dist/paths.js +33 -0
  25. package/dist/plugin.js +101 -0
  26. package/dist/report/check.js +252 -0
  27. package/dist/report/data.js +401 -0
  28. package/dist/report/export.js +105 -0
  29. package/dist/report/markdown.js +310 -0
  30. package/dist/report/meta.js +48 -0
  31. package/dist/report/pdf.js +96 -0
  32. package/dist/review-server.js +7 -0
  33. package/dist/review.js +274 -0
  34. package/dist/setup.js +121 -0
  35. package/dist/skills.js +81 -0
  36. package/dist/types.js +9 -0
  37. package/dist/uninstall.js +192 -0
  38. package/dist/versions.js +8 -0
  39. package/dist/watch-frame.js +593 -0
  40. package/dist/watch.js +873 -0
  41. package/dist/workspace.js +316 -0
  42. package/docs/GUIDE.md +323 -0
  43. package/hooks/autopilot.mjs +45 -0
  44. package/hooks/lib/autopilot.mjs +191 -0
  45. package/hooks/lib/briefing.mjs +750 -0
  46. package/hooks/lib/licence.mjs +452 -0
  47. package/hooks/lib/pre-pr.mjs +209 -0
  48. package/hooks/lib/sast.mjs +207 -0
  49. package/hooks/lib/setup.mjs +107 -0
  50. package/hooks/lib/workspace.mjs +78 -0
  51. package/hooks/pre-pr.mjs +54 -0
  52. package/hooks/session-start.mjs +82 -0
  53. package/package.json +49 -4
  54. package/skills/bravos/SKILL.md +63 -0
  55. package/skills/bravos-annotate/SKILL.md +56 -0
  56. package/skills/bravos-annotate/scripts/lib/pick.mjs +127 -0
  57. package/skills/bravos-annotate/scripts/slice.mjs +141 -0
  58. package/skills/bravos-campaign/SKILL.md +49 -0
  59. package/skills/bravos-cicd/SKILL.md +48 -0
  60. package/skills/bravos-codegraph/SKILL.md +57 -0
  61. package/skills/bravos-context/SKILL.md +51 -0
  62. package/skills/bravos-context/intent.example.md +48 -0
  63. package/skills/bravos-env/SKILL.md +48 -0
  64. package/skills/bravos-env/scope.example.yaml +50 -0
  65. package/skills/bravos-prove/SKILL.md +52 -0
  66. package/skills/bravos-recheck/SKILL.md +47 -0
  67. package/skills/bravos-report/SKILL.md +53 -0
  68. package/skills/bravos-report/assessment.md +54 -0
  69. package/skills/bravos-report/brief.md +23 -0
  70. package/skills/bravos-report/finding.md +74 -0
  71. package/skills/bravos-report/retest.md +29 -0
  72. package/skills/bravos-report/style.md +44 -0
  73. package/skills/bravos-sast/SKILL.md +48 -0
  74. package/skills/bravos-schedule/SKILL.md +44 -0
  75. package/skills/bravos-schedule/loop.md +16 -0
  76. package/skills/bravos-ship/SKILL.md +58 -0
  77. package/skills/bravos-templatize/SKILL.md +48 -0
  78. package/skills/bravos-templatize/operator-guidance.md +20 -0
  79. package/templates/omp/bravos.js +62 -0
  80. package/templates/opencode/bravos.js +33 -0
  81. package/templates/report/report.html +88 -0
  82. package/templates/review/review.html +327 -0
  83. package/templates/workflows/bravos-nightly-claude.yml +173 -0
  84. package/templates/workflows/bravos-nightly-codex.yml +172 -0
  85. package/templates/workflows/bravos-pr-gate.yml +142 -0
@@ -0,0 +1,172 @@
1
+ # bravos:generated — written by `bravos init --ci`; `bravos sync --ci` refreshes it while this line stays.
2
+ # The nightly "bulk bee": Codex covers the next slice of unannotated files with the
3
+ # bravos-annotate skill, the lint gate checks what it wrote, and the job opens one small PR.
4
+ # Optionally it then replays the committed retest kits against the target the reviewed
5
+ # .guardlink/scope.yaml authorizes (see the "Authorized scan (gated)" step); without that scope
6
+ # file's human-written attestation the scan step skips and the job annotates only.
7
+ # The nightly is on the Team plan: add the repo secret BRAVOS_LICENCE (the token from
8
+ # account.bugb.io/dashboard/license) so the agent's paid skills run; without it they say so and stop.
9
+ # Needs the repo secret OPENAI_API_KEY, and Settings > Actions > General > "Allow GitHub
10
+ # Actions to create and approve pull requests". Pin the actions to commit SHAs if your
11
+ # organisation requires it.
12
+ name: bravos nightly (Codex)
13
+
14
+ on:
15
+ schedule:
16
+ - cron: '17 3 * * 1-5'
17
+ workflow_dispatch:
18
+
19
+ permissions:
20
+ contents: write
21
+ pull-requests: write
22
+
23
+ concurrency:
24
+ group: bravos-nightly
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ annotate:
29
+ runs-on: ubuntu-latest
30
+ env:
31
+ BRAVOS_LICENCE: ${{ secrets.BRAVOS_LICENCE }}
32
+ timeout-minutes: 60
33
+ defaults:
34
+ run:
35
+ shell: bash
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ with:
39
+ fetch-depth: 0
40
+
41
+ - uses: actions/setup-node@v4
42
+ with:
43
+ node-version: 22
44
+
45
+ - name: Install guardlink
46
+ run: npm install -g guardlink
47
+
48
+ - name: Starting point
49
+ id: start
50
+ run: |
51
+ echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
52
+ guardlink ci -f json > "$RUNNER_TEMP/ci-before.json" || true
53
+ echo "coverage=$(jq -r '.summary.coverage.percent // "unknown"' "$RUNNER_TEMP/ci-before.json")" >> "$GITHUB_OUTPUT"
54
+
55
+ - name: Annotate the next slice
56
+ uses: openai/codex-action@v1
57
+ with:
58
+ openai-api-key: ${{ secrets.OPENAI_API_KEY }}
59
+ permission-profile: ":workspace"
60
+ safety-strategy: drop-sudo
61
+ prompt: |
62
+ Follow .agents/skills/bravos-annotate/SKILL.md in slice mode (the default) to cover the
63
+ next unannotated files that `guardlink unannotated --json` lists. You are the agent: do
64
+ the work in this session and never run guardlink annotate with an agent flag. Stop when
65
+ `guardlink lint . --since ${{ steps.start.outputs.sha }} --json` reports 0 errors and
66
+ `guardlink validate .` passes. Leave the changes uncommitted; a later step commits them
67
+ and opens the pull request.
68
+
69
+ - name: Gate what the agent wrote
70
+ env:
71
+ START: ${{ steps.start.outputs.sha }}
72
+ run: |
73
+ guardlink validate .
74
+ guardlink lint . --since "$START" --json > "$RUNNER_TEMP/lint.json" || true
75
+ errors=$(jq -r '.summary.errors' "$RUNNER_TEMP/lint.json")
76
+ if [ "$errors" != "0" ]; then
77
+ echo "::error::guardlink lint reports $errors error(s) in what the agent wrote"
78
+ jq . "$RUNNER_TEMP/lint.json"
79
+ exit 1
80
+ fi
81
+
82
+ # Opt-in: replays the committed retest kits against the target the reviewed scope authorizes.
83
+ # It runs only when .guardlink/scope.yaml carries a human-written authorization_attestation and
84
+ # vars.BRAVOS_SCAN_TARGET is loopback (started by vars.BRAVOS_APP_START) or in the scope's
85
+ # host_allowlist. It replays kits only: no probe generation and no destructive classes.
86
+ - name: Authorized scan (gated)
87
+ id: scan
88
+ env:
89
+ TARGET: ${{ vars.BRAVOS_SCAN_TARGET }}
90
+ APP_START: ${{ vars.BRAVOS_APP_START }}
91
+ AUTH_PROFILE: ${{ vars.BRAVOS_SCAN_AUTH }}
92
+ AUTH_STATE: ${{ secrets.BRAVOS_AUTH_STATE }}
93
+ run: |
94
+ skip() { echo "::notice::nightly scan skipped: $1"; echo "ran=false" >> "$GITHUB_OUTPUT"; exit 0; }
95
+ scope=.guardlink/scope.yaml
96
+ [ -n "$TARGET" ] || skip "vars.BRAVOS_SCAN_TARGET is not set"
97
+ [ -f "$scope" ] || skip "$scope is not committed"
98
+ attest=$(sed -n 's/^authorization_attestation:[[:space:]]*//p' "$scope" | sed 's/^"\(.*\)"$/\1/')
99
+ [ -n "$attest" ] || skip "$scope has no authorization_attestation; a human must write it and get it reviewed"
100
+ ls .guardlink/cxg-replay/*.js >/dev/null 2>&1 || skip "no retest kits in .guardlink/cxg-replay/"
101
+ host=$(printf '%s' "$TARGET" | sed -E 's#^[a-z]+://##; s#[:/].*$##')
102
+ case "$host" in
103
+ 127.0.0.1|localhost|::1|'[::1]') loopback=yes ;;
104
+ *) loopback=no ;;
105
+ esac
106
+ if [ "$loopback" = no ] && ! grep -qF "$host" <(sed -n '/^host_allowlist:/,/^[a-z_]*:/p' "$scope"); then
107
+ skip "$host is neither loopback nor in the scope's host_allowlist"
108
+ fi
109
+ if [ "$loopback" = yes ]; then
110
+ [ -n "$APP_START" ] || skip "a loopback target needs vars.BRAVOS_APP_START to start the app"
111
+ bash -c "$APP_START"
112
+ for i in $(seq 1 60); do curl -fsS -o /dev/null "$TARGET" && break; sleep 2; done
113
+ fi
114
+ cargo install --locked cert-x-gen
115
+ cxg pentest install
116
+ auth=()
117
+ if [ -n "$AUTH_PROFILE" ]; then
118
+ [ -n "$AUTH_STATE" ] || skip "vars.BRAVOS_SCAN_AUTH is set but secrets.BRAVOS_AUTH_STATE is empty"
119
+ printf '%s' "$AUTH_STATE" | base64 -d > "$RUNNER_TEMP/state.json"
120
+ cxg pentest auth import --storage-state "$RUNNER_TEMP/state.json" --profile "$AUTH_PROFILE" --target "$TARGET"
121
+ auth=(--auth "$AUTH_PROFILE")
122
+ fi
123
+ guardlink sarif . --profile pentest -o whitebox/findings.sarif
124
+ run=".guardlink/pentest-findings/nightly-$(date -u +%Y%m%d)"
125
+ set +e
126
+ cxg pentest run --codebase . --target "$TARGET" "${auth[@]}" --template-dir .guardlink/cxg-replay/ --scope-file "$scope" --attestation "$attest" --session-dir "$run" --ci
127
+ code=$?
128
+ set -e
129
+ echo "exit=$code" >> "$GITHUB_OUTPUT"
130
+ case "$code" in
131
+ 0|2) guardlink hypothesis confirm --from-scan "$run/report.json" --write || true
132
+ echo "ran=true" >> "$GITHUB_OUTPUT" ;;
133
+ *) echo "::warning::cxg pentest run exited $code: nothing was proven"; echo "ran=false" >> "$GITHUB_OUTPUT" ;;
134
+ esac
135
+
136
+ - name: Open the pull request
137
+ env:
138
+ SCAN_RAN: ${{ steps.scan.outputs.ran }}
139
+ SCAN_EXIT: ${{ steps.scan.outputs.exit }}
140
+ SCAN_TARGET: ${{ vars.BRAVOS_SCAN_TARGET }}
141
+ GH_TOKEN: ${{ github.token }}
142
+ START: ${{ steps.start.outputs.sha }}
143
+ BEFORE: ${{ steps.start.outputs.coverage }}
144
+ run: |
145
+ if [ -z "$(git status --porcelain)" ]; then
146
+ echo "Nothing new to annotate."
147
+ exit 0
148
+ fi
149
+ guardlink ci -f json > "$RUNNER_TEMP/ci-after.json" || true
150
+ after=$(jq -r '.summary.coverage.percent // "unknown"' "$RUNNER_TEMP/ci-after.json")
151
+ base=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)
152
+ branch="bravos/nightly-$(date -u +%Y%m%d-%H%M)"
153
+ git config user.name "bravos-nightly[bot]"
154
+ git config user.email "bravos-nightly@users.noreply.github.com"
155
+ git switch -c "$branch"
156
+ git add -A
157
+ git commit -m "annotate: nightly coverage slice (${BEFORE}% -> ${after}%)"
158
+ git push origin "$branch"
159
+ {
160
+ echo "Nightly coverage slice by bravos-annotate: ${BEFORE}% -> ${after}%."
161
+ echo
162
+ echo "## Security gate"
163
+ echo "validate ✔ · lint 0 errors · exposures recorded by this slice are listed below and stay open"
164
+ if [ "$SCAN_RAN" = "true" ]; then
165
+ echo "Tested: retest kits replayed against $SCAN_TARGET (exit code $SCAN_EXIT); outcomes recorded in .guardlink/hypotheses.json."
166
+ else
167
+ echo "Not tested: the gated scan did not run (see the job's notices)."
168
+ fi
169
+ echo
170
+ guardlink diff "$START" --markdown || true
171
+ } > "$RUNNER_TEMP/body.md"
172
+ gh pr create --base "$base" --head "$branch" --title "annotate: nightly coverage slice" --body-file "$RUNNER_TEMP/body.md"
@@ -0,0 +1,142 @@
1
+ # bravos:generated — written by `bravos init --ci`; `bravos sync --ci` refreshes it while this line stays.
2
+ # The bravos-ship gate as a pull-request check, the same rulings as the opt-in pre-PR hook.
3
+ # Deterministic: it runs guardlink only, spends no model, and needs no secret. A gate input that
4
+ # fails or does not parse fails the job as "unknown", never passes. Pin the actions below to
5
+ # commit SHAs if your organisation requires it.
6
+ name: bravos security gate
7
+
8
+ on:
9
+ pull_request:
10
+ # edited: adding the disclosure to the PR body re-runs the gate.
11
+ types: [opened, synchronize, reopened, edited]
12
+
13
+ permissions:
14
+ contents: read
15
+ security-events: write
16
+ # upload-sarif reads the run's metadata; private repositories need this to upload.
17
+ actions: read
18
+
19
+ jobs:
20
+ gate:
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 20
23
+ defaults:
24
+ run:
25
+ shell: bash
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ with:
29
+ fetch-depth: 0
30
+
31
+ - uses: actions/setup-node@v4
32
+ with:
33
+ node-version: 22
34
+
35
+ - name: Install guardlink
36
+ run: npm install -g guardlink
37
+
38
+ - name: Merge base
39
+ id: base
40
+ env:
41
+ BASE_REF: ${{ github.base_ref }}
42
+ run: echo "sha=$(git merge-base HEAD "origin/$BASE_REF")" >> "$GITHUB_OUTPUT"
43
+
44
+ # Whole-token id matching shared by the disclosure and @ticket checks: `#api` is not `#api-key`.
45
+ - name: jq helpers
46
+ run: |
47
+ cat > "$RUNNER_TEMP/named.jq" <<'EOF'
48
+ def esc: gsub("(?<c>[.*+?^${}()|\\[\\]\\\\])"; "\\\(.c)");
49
+ def named($text; $id): ($id | type) == "string" and ($text | test("(?<![\\w#.-])" + ($id | esc) + "(?![\\w-])"));
50
+ EOF
51
+
52
+ - name: Threat model parses and resolves
53
+ run: guardlink validate .
54
+
55
+ - name: New claims meet the evidence bar
56
+ env:
57
+ BASE_SHA: ${{ steps.base.outputs.sha }}
58
+ run: |
59
+ status=0
60
+ guardlink lint . --since "$BASE_SHA" --json > lint.json || status=$?
61
+ if ! errors=$(jq -er '.summary.errors | numbers' lint.json); then
62
+ echo "::error::unknown: guardlink lint --since $BASE_SHA exited $status without guardlink.lint JSON"
63
+ exit 1
64
+ fi
65
+ if [ "$errors" != "0" ]; then
66
+ echo "::error::guardlink lint reports $errors error(s) in this PR's claims; apply each violation's fix (bravos-ship)."
67
+ jq '.violations' lint.json
68
+ exit 1
69
+ fi
70
+
71
+ # Each new unmitigated exposure must be disclosed in the PR body by both its threat and its
72
+ # asset id (bravos-ship's Security gate section lists them), so an honest @exposes + @audit is
73
+ # reported rather than blocked forever. The body reaches the shell through env, never inline,
74
+ # so its text cannot run as a command.
75
+ - name: No undisclosed new exposure
76
+ env:
77
+ PR_BODY: ${{ github.event.pull_request.body }}
78
+ BASE_SHA: ${{ steps.base.outputs.sha }}
79
+ run: |
80
+ if ! guardlink diff "$BASE_SHA" --json > diff.json; then
81
+ echo "::error::unknown: guardlink diff $BASE_SHA --json failed"
82
+ exit 1
83
+ fi
84
+ if ! jq -e '.newUnmitigatedExposures | arrays' diff.json > /dev/null; then
85
+ echo "::error::unknown: guardlink diff $BASE_SHA --json has no newUnmitigatedExposures"
86
+ exit 1
87
+ fi
88
+ jq -r '.newUnmitigatedExposures[] | "- \(.threat) on \(.asset) — \(.location.file):\(.location.line)"' diff.json >> "$GITHUB_STEP_SUMMARY"
89
+ jq -r -L "$RUNNER_TEMP" --arg body "$PR_BODY" 'include "named";
90
+ .newUnmitigatedExposures[] | select((named($body; .threat) and named($body; .asset)) | not)
91
+ | "\(.threat) on \(.asset) — \(.location.file):\(.location.line)"' diff.json > undisclosed.txt
92
+ if [ -s undisclosed.txt ]; then
93
+ echo "::error::This PR adds unmitigated exposure the PR body does not disclose. Name each threat and asset id in its Security gate section (bravos-ship writes it):"
94
+ cat undisclosed.txt
95
+ exit 1
96
+ fi
97
+
98
+ # A confirmed-exploitable claim on a file this PR changes blocks it, unless that same file
99
+ # carries a ticket line (the @ticket verb) for the same threat and asset (bravos-ship step 7).
100
+ - name: No confirmed exploit on a changed path
101
+ env:
102
+ BASE_SHA: ${{ steps.base.outputs.sha }}
103
+ run: |
104
+ if ! guardlink ci -f json > ci.json; then
105
+ echo "::error::unknown: guardlink ci -f json failed"
106
+ exit 1
107
+ fi
108
+ if ! jq -e '.confirmed | arrays' ci.json > /dev/null; then
109
+ echo "::error::unknown: guardlink ci -f json did not produce guardlink.ci JSON with confirmed[]"
110
+ exit 1
111
+ fi
112
+ git diff --name-only "$BASE_SHA"...HEAD > changed.txt
113
+ jq -r '.confirmed[] | [.location.file, (.location.line | tostring), .threat, .asset, (.severity // "unrated")] | @tsv' ci.json > confirmed.tsv
114
+ hits=""
115
+ while IFS=$'\t' read -r file line threat asset severity; do
116
+ grep -qxF -- "$file" changed.txt || continue
117
+ if [ -f "$file" ] && jq -Rne -L "$RUNNER_TEMP" --arg t "$threat" --arg a "$asset" 'include "named";
118
+ [inputs | select(contains("@ticket") and named(.; $t) and named(.; $a))] | length > 0' < "$file" > /dev/null; then
119
+ continue
120
+ fi
121
+ hits+="$severity $threat on $asset — $file:$line"$'\n'
122
+ done < confirmed.tsv
123
+ if [ -n "$hits" ]; then
124
+ echo "::error::Confirmed-exploitable claims on changed files, with no @ticket for them in that file:"
125
+ printf '%s' "$hits"
126
+ exit 1
127
+ fi
128
+
129
+ - name: Undefended paths (informational)
130
+ continue-on-error: true
131
+ run: guardlink paths --json > paths.json
132
+
133
+ - name: SARIF for code scanning
134
+ run: guardlink sarif . --profile github -o guardlink.sarif
135
+
136
+ # A fork's pull_request token cannot write security events; skip the upload there.
137
+ - name: Upload SARIF
138
+ if: github.event.pull_request.head.repo.full_name == github.repository
139
+ uses: github/codeql-action/upload-sarif@v3
140
+ with:
141
+ sarif_file: guardlink.sarif
142
+ category: guardlink